Merge branch 'main' into likes-market-tips
This commit is contained in:
commit
77d927a4ec
43
.github/workflows/lint.yml
vendored
Normal file
43
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
name: Run linter (remove unused imports)
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 3
|
||||||
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
|
|
||||||
|
# mqp - i generated a personal token to use for these writes -- it's unclear
|
||||||
|
# why, but the default token didn't work, even when i gave it max permissions
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Auto-lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
|
||||||
|
- name: Restore cached node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: '**/node_modules'
|
||||||
|
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
- name: Install missing dependencies
|
||||||
|
run: yarn install --prefer-offline --frozen-lockfile
|
||||||
|
- name: Run lint script
|
||||||
|
run: yarn lint
|
||||||
|
- name: Commit any lint changes
|
||||||
|
if: always()
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
commit_message: Auto-remove unused imports
|
||||||
|
branch: ${{ github.head_ref }}
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['lodash'],
|
plugins: ['lodash', 'unused-imports'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
ignorePatterns: ['lib'],
|
ignorePatterns: ['lib'],
|
||||||
env: {
|
env: {
|
||||||
|
@ -26,6 +26,7 @@ module.exports = {
|
||||||
caughtErrorsIgnorePattern: '^_',
|
caughtErrorsIgnorePattern: '^_',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -140,6 +140,8 @@ function getCpmmInvested(yourBets: Bet[]) {
|
||||||
const sortedBets = sortBy(yourBets, 'createdTime')
|
const sortedBets = sortBy(yourBets, 'createdTime')
|
||||||
for (const bet of sortedBets) {
|
for (const bet of sortedBets) {
|
||||||
const { outcome, shares, amount } = bet
|
const { outcome, shares, amount } = bet
|
||||||
|
if (floatingEqual(shares, 0)) continue
|
||||||
|
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
|
totalShares[outcome] = (totalShares[outcome] ?? 0) + shares
|
||||||
totalSpent[outcome] = (totalSpent[outcome] ?? 0) + amount
|
totalSpent[outcome] = (totalSpent[outcome] ?? 0) + amount
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { PortfolioMetrics, User } from './user'
|
import { PortfolioMetrics, User } from './user'
|
||||||
import { filterDefined } from './util/array'
|
import { filterDefined } from './util/array'
|
||||||
|
|
||||||
const LOAN_DAILY_RATE = 0.01
|
const LOAN_DAILY_RATE = 0.02
|
||||||
|
|
||||||
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
|
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
|
||||||
const netValue = investedValue - loanTotal
|
const netValue = investedValue - loanTotal
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "2.0.0-beta.181",
|
"@tiptap/core": "2.0.0-beta.182",
|
||||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
12
common/post.ts
Normal file
12
common/post.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: JSONContent
|
||||||
|
creatorId: string // User id
|
||||||
|
createdTime: number
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_POST_TITLE_LENGTH = 480
|
|
@ -44,6 +44,7 @@ export type User = {
|
||||||
currentBettingStreak?: number
|
currentBettingStreak?: number
|
||||||
hasSeenContractFollowModal?: boolean
|
hasSeenContractFollowModal?: boolean
|
||||||
freeMarketsCreated?: number
|
freeMarketsCreated?: number
|
||||||
|
isBannedFromPosting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PrivateUser = {
|
export type PrivateUser = {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const HOUR_MS = 60 * 60 * 1000
|
export const MINUTE_MS = 60 * 1000
|
||||||
|
export const HOUR_MS = 60 * MINUTE_MS
|
||||||
export const DAY_MS = 24 * HOUR_MS
|
export const DAY_MS = 24 * HOUR_MS
|
||||||
|
|
|
@ -40,6 +40,10 @@
|
||||||
"collectionGroup": "comments",
|
"collectionGroup": "comments",
|
||||||
"queryScope": "COLLECTION_GROUP",
|
"queryScope": "COLLECTION_GROUP",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "commentType",
|
||||||
|
"order": "ASCENDING"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "userId",
|
"fieldPath": "userId",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
|
|
|
@ -180,5 +180,14 @@ service cloud.firestore {
|
||||||
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember();
|
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /posts/{postId} {
|
||||||
|
allow read;
|
||||||
|
allow update: if request.auth.uid == resource.data.creatorId
|
||||||
|
&& request.resource.data.diff(resource.data)
|
||||||
|
.affectedKeys()
|
||||||
|
.hasOnly(['name', 'content']);
|
||||||
|
allow delete: if request.auth.uid == resource.data.creatorId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['lodash'],
|
plugins: ['lodash', 'unused-imports'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
ignorePatterns: ['dist', 'lib'],
|
ignorePatterns: ['dist', 'lib'],
|
||||||
env: {
|
env: {
|
||||||
|
@ -26,6 +26,7 @@ module.exports = {
|
||||||
caughtErrorsIgnorePattern: '^_',
|
caughtErrorsIgnorePattern: '^_',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -26,11 +26,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/node": "1.10.0",
|
"@amplitude/node": "1.10.0",
|
||||||
"@google-cloud/functions-framework": "3.1.2",
|
"@google-cloud/functions-framework": "3.1.2",
|
||||||
"@tiptap/core": "2.0.0-beta.181",
|
"@tiptap/core": "2.0.0-beta.182",
|
||||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dayjs": "1.11.4",
|
"dayjs": "1.11.4",
|
||||||
"express": "4.18.1",
|
"express": "4.18.1",
|
||||||
|
|
83
functions/src/create-post.ts
Normal file
83
functions/src/create-post.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { getUser } from './utils'
|
||||||
|
import { slugify } from '../../common/util/slugify'
|
||||||
|
import { randomString } from '../../common/util/random'
|
||||||
|
import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post'
|
||||||
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
import { JSONContent } from '@tiptap/core'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
|
z.intersection(
|
||||||
|
z.record(z.any()),
|
||||||
|
z.object({
|
||||||
|
type: z.string().optional(),
|
||||||
|
attrs: z.record(z.any()).optional(),
|
||||||
|
content: z.array(contentSchema).optional(),
|
||||||
|
marks: z
|
||||||
|
.array(
|
||||||
|
z.intersection(
|
||||||
|
z.record(z.any()),
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
attrs: z.record(z.any()).optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
text: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const postSchema = z.object({
|
||||||
|
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
||||||
|
content: contentSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
const { title, content } = validate(postSchema, req.body)
|
||||||
|
|
||||||
|
const creator = await getUser(auth.uid)
|
||||||
|
if (!creator)
|
||||||
|
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||||
|
|
||||||
|
console.log('creating post owned by', creator.username, 'titled', title)
|
||||||
|
|
||||||
|
const slug = await getSlug(title)
|
||||||
|
|
||||||
|
const postRef = firestore.collection('posts').doc()
|
||||||
|
|
||||||
|
const post: Post = {
|
||||||
|
id: postRef.id,
|
||||||
|
creatorId: creator.id,
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
await postRef.create(post)
|
||||||
|
|
||||||
|
return { status: 'success', post }
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getSlug = async (title: string) => {
|
||||||
|
const proposedSlug = slugify(title)
|
||||||
|
|
||||||
|
const preexistingPost = await getPostFromSlug(proposedSlug)
|
||||||
|
|
||||||
|
return preexistingPost ? proposedSlug + '-' + randomString() : proposedSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPostFromSlug(slug: string) {
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
const snap = await firestore
|
||||||
|
.collection('posts')
|
||||||
|
.where('slug', '==', slug)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return snap.empty ? undefined : (snap.docs[0].data() as Post)
|
||||||
|
}
|
|
@ -1,84 +1,91 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html style="
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
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">
|
<head>
|
||||||
img {
|
<meta name="viewport" content="width=device-width" />
|
||||||
max-width: 100%;
|
<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;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
h1 {
|
||||||
-webkit-font-smoothing: antialiased;
|
font-weight: 800 !important;
|
||||||
-webkit-text-size-adjust: none;
|
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;
|
width: 100% !important;
|
||||||
height: 100%;
|
|
||||||
line-height: 1.6em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.content {
|
||||||
background-color: #f6f6f6;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 640px) {
|
.content-wrap {
|
||||||
body {
|
padding: 10px !important;
|
||||||
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
|
.invoice {
|
||||||
itemscope
|
width: 100% !important;
|
||||||
itemtype="http://schema.org/EmailMessage"
|
}
|
||||||
style="
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -89,43 +96,29 @@
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<table class="body-wrap" style="
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="body-wrap"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
<td class="container" width="600" style="
|
||||||
></td>
|
|
||||||
<td
|
|
||||||
class="container"
|
|
||||||
width="600"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -134,12 +127,8 @@
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div class="content" style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -147,14 +136,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
<table
|
|
||||||
class="main"
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -162,20 +145,14 @@
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid #e9e9e9;
|
border: 1px solid #e9e9e9;
|
||||||
"
|
" bgcolor="#fff">
|
||||||
bgcolor="#fff"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-wrap aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-wrap aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -183,35 +160,23 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -220,29 +185,21 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 0px 0;
|
padding: 0 0 0px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="https://manifold.markets" target="_blank">
|
||||||
>
|
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||||
<img
|
alt="Manifold Markets" />
|
||||||
src="https://manifold.markets/logo-banner.png"
|
</a>
|
||||||
width="300"
|
</td>
|
||||||
style="height: auto"
|
</tr>
|
||||||
alt="Manifold Markets"
|
<tr style="
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-block aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -251,13 +208,8 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table class="invoice" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="invoice"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -266,19 +218,15 @@
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -286,37 +234,26 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div>
|
||||||
>
|
<img src="{{avatarUrl}}" width="30" height="30" style="
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src="{{avatarUrl}}"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
style="
|
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
"
|
" alt="" />
|
||||||
alt=""
|
{{name}}
|
||||||
/>
|
</div>
|
||||||
{{name}}
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -324,40 +261,29 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<span style="white-space: pre-line">{{answer}}</span>
|
||||||
<span style="white-space: pre-line"
|
</div>
|
||||||
>{{answer}}</span
|
</td>
|
||||||
>
|
</tr>
|
||||||
</div>
|
<tr style="
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="padding: 20px 0 0 0; margin: 0">
|
||||||
<td style="padding: 20px 0 0 0; margin: 0">
|
<div align="center">
|
||||||
<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]-->
|
||||||
<!--[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="
|
||||||
<a
|
|
||||||
href="{{marketUrl}}"
|
|
||||||
target="_blank"
|
|
||||||
style="
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: arial, helvetica, sans-serif;
|
font-family: arial, helvetica, sans-serif;
|
||||||
|
@ -375,38 +301,29 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
mso-border-alt: none;
|
mso-border-alt: none;
|
||||||
"
|
">
|
||||||
>
|
<span style="
|
||||||
<span
|
|
||||||
style="
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
line-height: 120%;
|
line-height: 120%;
|
||||||
"
|
"><span style="
|
||||||
><span
|
|
||||||
style="
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 18.8px;
|
line-height: 18.8px;
|
||||||
"
|
">View answer</span></span>
|
||||||
>View answer</span
|
</a>
|
||||||
></span
|
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||||
>
|
</div>
|
||||||
</a>
|
</td>
|
||||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
<div class="footer" style="
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="footer"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -415,28 +332,20 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table width="100%" style="
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="aligncenter content-block" style="
|
||||||
<td
|
|
||||||
class="aligncenter content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -446,14 +355,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
Questions? Come ask in
|
||||||
valign="top"
|
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||||
>
|
|
||||||
Questions? Come ask in
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/eHQBNBqXuh"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -461,12 +365,8 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">our Discord</a>! Or,
|
||||||
>our Discord</a
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
>! Or,
|
|
||||||
<a
|
|
||||||
href="{{unsubscribeUrl}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -474,26 +374,22 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">unsubscribe</a>.
|
||||||
>unsubscribe</a
|
</td>
|
||||||
>.
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</td>
|
||||||
style="
|
<td style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
</tr>
|
||||||
></td>
|
</table>
|
||||||
</tr>
|
</body>
|
||||||
</table>
|
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
|
@ -1,84 +1,91 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html style="
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
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">
|
<head>
|
||||||
img {
|
<meta name="viewport" content="width=device-width" />
|
||||||
max-width: 100%;
|
<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;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
h1 {
|
||||||
-webkit-font-smoothing: antialiased;
|
font-weight: 800 !important;
|
||||||
-webkit-text-size-adjust: none;
|
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;
|
width: 100% !important;
|
||||||
height: 100%;
|
|
||||||
line-height: 1.6em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.content {
|
||||||
background-color: #f6f6f6;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 640px) {
|
.content-wrap {
|
||||||
body {
|
padding: 10px !important;
|
||||||
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
|
.invoice {
|
||||||
itemscope
|
width: 100% !important;
|
||||||
itemtype="http://schema.org/EmailMessage"
|
}
|
||||||
style="
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -89,43 +96,29 @@
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<table class="body-wrap" style="
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="body-wrap"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
<td class="container" width="600" style="
|
||||||
></td>
|
|
||||||
<td
|
|
||||||
class="container"
|
|
||||||
width="600"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -134,12 +127,8 @@
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div class="content" style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -147,14 +136,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
<table
|
|
||||||
class="main"
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -162,20 +145,14 @@
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid #e9e9e9;
|
border: 1px solid #e9e9e9;
|
||||||
"
|
" bgcolor="#fff">
|
||||||
bgcolor="#fff"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-wrap aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-wrap aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -183,35 +160,23 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -220,30 +185,22 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 40px 0;
|
padding: 0 0 40px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="https://manifold.markets" target="_blank">
|
||||||
>
|
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||||
<img
|
alt="Manifold Markets" />
|
||||||
src="https://manifold.markets/logo-banner.png"
|
</a>
|
||||||
width="300"
|
</td>
|
||||||
style="height: auto"
|
</tr>
|
||||||
alt="Manifold Markets"
|
<tr style="
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -252,24 +209,18 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 6px 0;
|
padding: 0 0 6px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
You asked
|
||||||
>
|
</td>
|
||||||
You asked
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -277,12 +228,8 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="{{url}}" style="
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="{{url}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
'Lucida Grande', sans-serif;
|
'Lucida Grande', sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -295,24 +242,18 @@
|
||||||
color: #4337c9;
|
color: #4337c9;
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
"
|
">
|
||||||
>
|
{{question}}</a>
|
||||||
{{question}}</a
|
</td>
|
||||||
>
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -320,12 +261,8 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 0px;
|
padding: 0 0 0px;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<h2 class="aligncenter" style="
|
||||||
>
|
|
||||||
<h2
|
|
||||||
class="aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
'Lucida Grande', sans-serif;
|
'Lucida Grande', sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -335,25 +272,19 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
"
|
" align="center">
|
||||||
align="center"
|
Market closed
|
||||||
>
|
</h2>
|
||||||
Market closed
|
</td>
|
||||||
</h2>
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-block aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -362,13 +293,8 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table class="invoice" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="invoice"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -376,19 +302,15 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -396,116 +318,91 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
Hi {{name}},
|
||||||
>
|
<br style="
|
||||||
Hi {{name}},
|
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
A market you created has closed. It's attracted
|
||||||
A market you created has closed. It's attracted
|
<span style="font-weight: bold">{{volume}}</span>
|
||||||
<span style="font-weight: bold">{{volume}}</span>
|
in bets — congrats!
|
||||||
in bets — congrats!
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Resolve your market to earn {{creatorFee}} as the
|
||||||
Resolve your market to earn {{creatorFee}} as the
|
creator commission.
|
||||||
creator commission.
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Thanks,
|
||||||
Thanks,
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Manifold Team
|
||||||
Manifold Team
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="padding: 10px 0 0 0; margin: 0">
|
||||||
<td style="padding: 10px 0 0 0; margin: 0">
|
<div align="center">
|
||||||
<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]-->
|
||||||
<!--[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="
|
||||||
<a
|
|
||||||
href="{{url}}"
|
|
||||||
target="_blank"
|
|
||||||
style="
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: arial, helvetica, sans-serif;
|
font-family: arial, helvetica, sans-serif;
|
||||||
|
@ -523,38 +420,29 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
mso-border-alt: none;
|
mso-border-alt: none;
|
||||||
"
|
">
|
||||||
>
|
<span style="
|
||||||
<span
|
|
||||||
style="
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
line-height: 120%;
|
line-height: 120%;
|
||||||
"
|
"><span style="
|
||||||
><span
|
|
||||||
style="
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 18.8px;
|
line-height: 18.8px;
|
||||||
"
|
">View market</span></span>
|
||||||
>View market</span
|
</a>
|
||||||
></span
|
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||||
>
|
</div>
|
||||||
</a>
|
</td>
|
||||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
<div class="footer" style="
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="footer"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -563,28 +451,20 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table width="100%" style="
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="aligncenter content-block" style="
|
||||||
<td
|
|
||||||
class="aligncenter content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -594,14 +474,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
Questions? Come ask in
|
||||||
valign="top"
|
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||||
>
|
|
||||||
Questions? Come ask in
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/eHQBNBqXuh"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -609,12 +484,8 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">our Discord</a>! Or,
|
||||||
>our Discord</a
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
>! Or,
|
|
||||||
<a
|
|
||||||
href="{{unsubscribeUrl}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -622,26 +493,22 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">unsubscribe</a>.
|
||||||
>unsubscribe</a
|
</td>
|
||||||
>.
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</td>
|
||||||
style="
|
<td style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
</tr>
|
||||||
></td>
|
</table>
|
||||||
</tr>
|
</body>
|
||||||
</table>
|
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
|
@ -1,84 +1,91 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html style="
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
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">
|
<head>
|
||||||
img {
|
<meta name="viewport" content="width=device-width" />
|
||||||
max-width: 100%;
|
<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;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
h1 {
|
||||||
-webkit-font-smoothing: antialiased;
|
font-weight: 800 !important;
|
||||||
-webkit-text-size-adjust: none;
|
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;
|
width: 100% !important;
|
||||||
height: 100%;
|
|
||||||
line-height: 1.6em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.content {
|
||||||
background-color: #f6f6f6;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 640px) {
|
.content-wrap {
|
||||||
body {
|
padding: 10px !important;
|
||||||
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
|
.invoice {
|
||||||
itemscope
|
width: 100% !important;
|
||||||
itemtype="http://schema.org/EmailMessage"
|
}
|
||||||
style="
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -89,43 +96,29 @@
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<table class="body-wrap" style="
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="body-wrap"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
<td class="container" width="600" style="
|
||||||
></td>
|
|
||||||
<td
|
|
||||||
class="container"
|
|
||||||
width="600"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -134,12 +127,8 @@
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div class="content" style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -147,14 +136,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
<table
|
|
||||||
class="main"
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -162,20 +145,14 @@
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid #e9e9e9;
|
border: 1px solid #e9e9e9;
|
||||||
"
|
" bgcolor="#fff">
|
||||||
bgcolor="#fff"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-wrap aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-wrap aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -183,35 +160,23 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -220,29 +185,21 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 0px 0;
|
padding: 0 0 0px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="https://manifold.markets" target="_blank">
|
||||||
>
|
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||||
<img
|
alt="Manifold Markets" />
|
||||||
src="https://manifold.markets/logo-banner.png"
|
</a>
|
||||||
width="300"
|
</td>
|
||||||
style="height: auto"
|
</tr>
|
||||||
alt="Manifold Markets"
|
<tr style="
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-block aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -251,13 +208,8 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table class="invoice" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="invoice"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -266,59 +218,42 @@
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div>
|
||||||
>
|
<img src="{{commentorAvatarUrl}}" width="30" height="30" style="
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src="{{commentorAvatarUrl}}"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
style="
|
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
"
|
" alt="" />
|
||||||
alt=""
|
<span style="font-weight: bold">{{commentorName}}</span>
|
||||||
/>
|
{{betDescription}}
|
||||||
<span style="font-weight: bold"
|
</div>
|
||||||
>{{commentorName}}</span
|
</td>
|
||||||
>
|
</tr>
|
||||||
{{betDescription}}
|
<tr style="
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -326,40 +261,29 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<span style="white-space: pre-line">{{comment}}</span>
|
||||||
<span style="white-space: pre-line"
|
</div>
|
||||||
>{{comment}}</span
|
</td>
|
||||||
>
|
</tr>
|
||||||
</div>
|
<tr style="
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="padding: 20px 0 0 0; margin: 0">
|
||||||
<td style="padding: 20px 0 0 0; margin: 0">
|
<div align="center">
|
||||||
<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]-->
|
||||||
<!--[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="
|
||||||
<a
|
|
||||||
href="{{marketUrl}}"
|
|
||||||
target="_blank"
|
|
||||||
style="
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: arial, helvetica, sans-serif;
|
font-family: arial, helvetica, sans-serif;
|
||||||
|
@ -377,38 +301,29 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
mso-border-alt: none;
|
mso-border-alt: none;
|
||||||
"
|
">
|
||||||
>
|
<span style="
|
||||||
<span
|
|
||||||
style="
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
line-height: 120%;
|
line-height: 120%;
|
||||||
"
|
"><span style="
|
||||||
><span
|
|
||||||
style="
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 18.8px;
|
line-height: 18.8px;
|
||||||
"
|
">View comment</span></span>
|
||||||
>View comment</span
|
</a>
|
||||||
></span
|
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||||
>
|
</div>
|
||||||
</a>
|
</td>
|
||||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
<div class="footer" style="
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="footer"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -417,28 +332,20 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table width="100%" style="
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="aligncenter content-block" style="
|
||||||
<td
|
|
||||||
class="aligncenter content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -448,14 +355,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
Questions? Come ask in
|
||||||
valign="top"
|
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||||
>
|
|
||||||
Questions? Come ask in
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/eHQBNBqXuh"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -463,12 +365,8 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">our Discord</a>! Or,
|
||||||
>our Discord</a
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
>! Or,
|
|
||||||
<a
|
|
||||||
href="{{unsubscribeUrl}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -476,26 +374,22 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">unsubscribe</a>.
|
||||||
>unsubscribe</a
|
</td>
|
||||||
>.
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</td>
|
||||||
style="
|
<td style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
</tr>
|
||||||
></td>
|
</table>
|
||||||
</tr>
|
</body>
|
||||||
</table>
|
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
|
@ -1,84 +1,91 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html style="
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
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">
|
<head>
|
||||||
img {
|
<meta name="viewport" content="width=device-width" />
|
||||||
max-width: 100%;
|
<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;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
h1 {
|
||||||
-webkit-font-smoothing: antialiased;
|
font-weight: 800 !important;
|
||||||
-webkit-text-size-adjust: none;
|
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;
|
width: 100% !important;
|
||||||
height: 100%;
|
|
||||||
line-height: 1.6em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.content {
|
||||||
background-color: #f6f6f6;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 640px) {
|
.content-wrap {
|
||||||
body {
|
padding: 10px !important;
|
||||||
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
|
.invoice {
|
||||||
itemscope
|
width: 100% !important;
|
||||||
itemtype="http://schema.org/EmailMessage"
|
}
|
||||||
style="
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body itemscope itemtype="http://schema.org/EmailMessage" style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -89,43 +96,29 @@
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<table class="body-wrap" style="
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="body-wrap"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" bgcolor="#f6f6f6">
|
||||||
bgcolor="#f6f6f6"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
<td class="container" width="600" style="
|
||||||
></td>
|
|
||||||
<td
|
|
||||||
class="container"
|
|
||||||
width="600"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -134,12 +127,8 @@
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<div class="content" style="
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -147,14 +136,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
<table
|
|
||||||
class="main"
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -162,20 +145,14 @@
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid #e9e9e9;
|
border: 1px solid #e9e9e9;
|
||||||
"
|
" bgcolor="#fff">
|
||||||
bgcolor="#fff"
|
<tr style="
|
||||||
>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-wrap aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-wrap aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -183,35 +160,23 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table width="100%" cellpadding="0" cellspacing="0" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -220,30 +185,22 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 40px 0;
|
padding: 0 0 40px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="https://manifold.markets" target="_blank">
|
||||||
>
|
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
|
||||||
<img
|
alt="Manifold Markets" />
|
||||||
src="https://manifold.markets/logo-banner.png"
|
</a>
|
||||||
width="300"
|
</td>
|
||||||
style="height: auto"
|
</tr>
|
||||||
alt="Manifold Markets"
|
<tr style="
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -252,24 +209,18 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 6px 0;
|
padding: 0 0 6px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
{{creatorName}} asked
|
||||||
>
|
</td>
|
||||||
{{creatorName}} asked
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -277,12 +228,8 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<a href="{{url}}" style="
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="{{url}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
'Lucida Grande', sans-serif;
|
'Lucida Grande', sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -295,24 +242,18 @@
|
||||||
color: #4337c9;
|
color: #4337c9;
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
"
|
">
|
||||||
>
|
{{question}}</a>
|
||||||
{{question}}</a
|
</td>
|
||||||
>
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block" style="
|
||||||
<td
|
|
||||||
class="content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -320,12 +261,8 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 0px;
|
padding: 0 0 0px;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
<h2 class="aligncenter" style="
|
||||||
>
|
|
||||||
<h2
|
|
||||||
class="aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
'Lucida Grande', sans-serif;
|
'Lucida Grande', sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -335,25 +272,19 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
"
|
" align="center">
|
||||||
align="center"
|
Resolved {{outcome}}
|
||||||
>
|
</h2>
|
||||||
Resolved {{outcome}}
|
</td>
|
||||||
</h2>
|
</tr>
|
||||||
</td>
|
<tr style="
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="content-block aligncenter" style="
|
||||||
<td
|
|
||||||
class="content-block aligncenter"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -362,13 +293,8 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
<table class="invoice" style="
|
||||||
valign="top"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
class="invoice"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -376,19 +302,15 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -396,138 +318,105 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
"
|
" valign="top">
|
||||||
valign="top"
|
Dear {{name}},
|
||||||
>
|
<br style="
|
||||||
Dear {{name}},
|
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
A market you bet in has been resolved!
|
||||||
A market you bet in has been resolved!
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Your investment was
|
||||||
Your investment was
|
<span style="font-weight: bold">{{investment}}</span>.
|
||||||
<span style="font-weight: bold"
|
<br style="
|
||||||
>M$ {{investment}}</span
|
|
||||||
>.
|
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Your payout is
|
||||||
Your payout is
|
<span style="font-weight: bold">{{payout}}</span>.
|
||||||
<span style="font-weight: bold"
|
<br style="
|
||||||
>M$ {{payout}}</span
|
|
||||||
>.
|
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Thanks,
|
||||||
Thanks,
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
Manifold Team
|
||||||
Manifold Team
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
<br style="
|
||||||
<br
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica,
|
font-family: 'Helvetica Neue', Helvetica,
|
||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" />
|
||||||
/>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td style="padding: 10px 0 0 0; margin: 0">
|
||||||
<td style="padding: 10px 0 0 0; margin: 0">
|
<div align="center">
|
||||||
<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]-->
|
||||||
<!--[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="
|
||||||
<a
|
|
||||||
href="{{url}}"
|
|
||||||
target="_blank"
|
|
||||||
style="
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: arial, helvetica, sans-serif;
|
font-family: arial, helvetica, sans-serif;
|
||||||
|
@ -545,38 +434,29 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
mso-border-alt: none;
|
mso-border-alt: none;
|
||||||
"
|
">
|
||||||
>
|
<span style="
|
||||||
<span
|
|
||||||
style="
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
line-height: 120%;
|
line-height: 120%;
|
||||||
"
|
"><span style="
|
||||||
><span
|
|
||||||
style="
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 18.8px;
|
line-height: 18.8px;
|
||||||
"
|
">View market</span></span>
|
||||||
>View market</span
|
</a>
|
||||||
></span
|
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
||||||
>
|
</div>
|
||||||
</a>
|
</td>
|
||||||
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
<div class="footer" style="
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="footer"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -585,28 +465,20 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
"
|
">
|
||||||
>
|
<table width="100%" style="
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<tr style="
|
||||||
<tr
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">
|
||||||
>
|
<td class="aligncenter content-block" style="
|
||||||
<td
|
|
||||||
class="aligncenter content-block"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -616,14 +488,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 0 20px;
|
padding: 0 0 20px;
|
||||||
"
|
" align="center" valign="top">
|
||||||
align="center"
|
Questions? Come ask in
|
||||||
valign="top"
|
<a href="https://discord.gg/eHQBNBqXuh" style="
|
||||||
>
|
|
||||||
Questions? Come ask in
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/eHQBNBqXuh"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -631,12 +498,8 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">our Discord</a>! Or,
|
||||||
>our Discord</a
|
<a href="{{unsubscribeUrl}}" style="
|
||||||
>! Or,
|
|
||||||
<a
|
|
||||||
href="{{unsubscribeUrl}}"
|
|
||||||
style="
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial,
|
font-family: 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -644,26 +507,22 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
">unsubscribe</a>.
|
||||||
>unsubscribe</a
|
</td>
|
||||||
>.
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</td>
|
||||||
style="
|
<td style="
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
"
|
" valign="top"></td>
|
||||||
valign="top"
|
</tr>
|
||||||
></td>
|
</table>
|
||||||
</tr>
|
</body>
|
||||||
</table>
|
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
|
@ -53,22 +53,29 @@ export const sendMarketResolutionEmail = async (
|
||||||
|
|
||||||
const subject = `Resolved ${outcome}: ${contract.question}`
|
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||||
|
|
||||||
// const creatorPayoutText =
|
const creatorPayoutText =
|
||||||
// userId === creator.id
|
creatorPayout >= 1 && userId === creator.id
|
||||||
// ? ` (plus ${formatMoney(creatorPayout)} in commissions)`
|
? ` (plus ${formatMoney(creatorPayout)} in commissions)`
|
||||||
// : ''
|
: ''
|
||||||
|
|
||||||
const emailType = 'market-resolved'
|
const emailType = 'market-resolved'
|
||||||
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
|
||||||
|
|
||||||
|
const displayedInvestment =
|
||||||
|
Number.isNaN(investment) || investment < 0
|
||||||
|
? formatMoney(0)
|
||||||
|
: formatMoney(investment)
|
||||||
|
|
||||||
|
const displayedPayout = formatMoney(payout)
|
||||||
|
|
||||||
const templateData: market_resolved_template = {
|
const templateData: market_resolved_template = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
creatorName: creator.name,
|
creatorName: creator.name,
|
||||||
question: contract.question,
|
question: contract.question,
|
||||||
outcome,
|
outcome,
|
||||||
investment: `${Math.floor(investment)}`,
|
investment: displayedInvestment,
|
||||||
payout: `${Math.floor(payout)}`,
|
payout: displayedPayout + creatorPayoutText,
|
||||||
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { acceptchallenge } from './accept-challenge'
|
import { acceptchallenge } from './accept-challenge'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { createpost } from './create-post'
|
||||||
|
|
||||||
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
const toCloudFunction = ({ opts, handler }: EndpointDefinition) => {
|
||||||
return onRequest(opts, handler as any)
|
return onRequest(opts, handler as any)
|
||||||
|
@ -98,6 +99,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession)
|
||||||
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
const getCurrentUserFunction = toCloudFunction(getcurrentuser)
|
||||||
const acceptChallenge = toCloudFunction(acceptchallenge)
|
const acceptChallenge = toCloudFunction(acceptchallenge)
|
||||||
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
const getCustomTokenFunction = toCloudFunction(getcustomtoken)
|
||||||
|
const createPostFunction = toCloudFunction(createpost)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
healthFunction as health,
|
healthFunction as health,
|
||||||
|
@ -121,4 +123,5 @@ export {
|
||||||
getCurrentUserFunction as getcurrentuser,
|
getCurrentUserFunction as getcurrentuser,
|
||||||
acceptChallenge as acceptchallenge,
|
acceptChallenge as acceptchallenge,
|
||||||
getCustomTokenFunction as getcustomtoken,
|
getCustomTokenFunction as getcustomtoken,
|
||||||
|
createPostFunction as createpost,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { getRedeemableAmount, getRedemptionBets } from '../../common/redeem'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
import { floatingEqual } from '../../common/util/math'
|
||||||
|
|
||||||
export const redeemShares = async (userId: string, contractId: string) => {
|
export const redeemShares = async (userId: string, contractId: string) => {
|
||||||
return await firestore.runTransaction(async (trans) => {
|
return await firestore.runTransaction(async (trans) => {
|
||||||
|
@ -21,7 +22,7 @@ export const redeemShares = async (userId: string, contractId: string) => {
|
||||||
const betsSnap = await trans.get(betsColl.where('userId', '==', userId))
|
const betsSnap = await trans.get(betsColl.where('userId', '==', userId))
|
||||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
const { shares, loanPayment, netAmount } = getRedeemableAmount(bets)
|
const { shares, loanPayment, netAmount } = getRedeemableAmount(bets)
|
||||||
if (netAmount === 0) {
|
if (floatingEqual(netAmount, 0)) {
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract)
|
const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract)
|
||||||
|
|
39
functions/src/scripts/backfill-unique-bettors.ts
Normal file
39
functions/src/scripts/backfill-unique-bettors.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
import { getValues, log, writeAsync } from '../utils'
|
||||||
|
import { Bet } from '../../../common/bet'
|
||||||
|
import { groupBy, mapValues, sortBy, uniq } from 'lodash'
|
||||||
|
|
||||||
|
initAdmin()
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
const getBettorsByContractId = async () => {
|
||||||
|
const bets = await getValues<Bet>(firestore.collectionGroup('bets'))
|
||||||
|
log(`Loaded ${bets.length} bets.`)
|
||||||
|
const betsByContractId = groupBy(bets, 'contractId')
|
||||||
|
return mapValues(betsByContractId, (bets) =>
|
||||||
|
uniq(sortBy(bets, 'createdTime').map((bet) => bet.userId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUniqueBettors = async () => {
|
||||||
|
const bettorsByContractId = await getBettorsByContractId()
|
||||||
|
|
||||||
|
const updates = Object.entries(bettorsByContractId).map(
|
||||||
|
([contractId, userIds]) => {
|
||||||
|
const update = {
|
||||||
|
uniqueBettorIds: userIds,
|
||||||
|
uniqueBettorCount: userIds.length,
|
||||||
|
}
|
||||||
|
const docRef = firestore.collection('contracts').doc(contractId)
|
||||||
|
return { doc: docRef, fields: update }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
log(`Updating ${updates.length} contracts.`)
|
||||||
|
await writeAsync(firestore, updates)
|
||||||
|
log(`Updated all contracts.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
updateUniqueBettors()
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe'
|
||||||
import { stripewebhook, createcheckoutsession } from './stripe'
|
import { stripewebhook, createcheckoutsession } from './stripe'
|
||||||
import { getcurrentuser } from './get-current-user'
|
import { getcurrentuser } from './get-current-user'
|
||||||
import { getcustomtoken } from './get-custom-token'
|
import { getcustomtoken } from './get-custom-token'
|
||||||
|
import { createpost } from './create-post'
|
||||||
|
|
||||||
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
type Middleware = (req: Request, res: Response, next: NextFunction) => void
|
||||||
const app = express()
|
const app = express()
|
||||||
|
@ -67,6 +68,7 @@ addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession)
|
||||||
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
addJsonEndpointRoute('/getcurrentuser', getcurrentuser)
|
||||||
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
addEndpointRoute('/getcustomtoken', getcustomtoken)
|
||||||
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
addEndpointRoute('/stripewebhook', stripewebhook, express.raw())
|
||||||
|
addEndpointRoute('/createpost', createpost)
|
||||||
|
|
||||||
app.listen(PORT)
|
app.listen(PORT)
|
||||||
console.log(`Serving functions on port ${PORT}.`)
|
console.log(`Serving functions on port ${PORT}.`)
|
||||||
|
|
|
@ -55,16 +55,18 @@ export const updateMetricsCore = async () => {
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const betsByContract = groupBy(bets, (bet) => bet.contractId)
|
const betsByContract = groupBy(bets, (bet) => bet.contractId)
|
||||||
const contractUpdates = contracts.map((contract) => {
|
const contractUpdates = contracts
|
||||||
const contractBets = betsByContract[contract.id] ?? []
|
.filter((contract) => contract.id)
|
||||||
return {
|
.map((contract) => {
|
||||||
doc: firestore.collection('contracts').doc(contract.id),
|
const contractBets = betsByContract[contract.id] ?? []
|
||||||
fields: {
|
return {
|
||||||
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
doc: firestore.collection('contracts').doc(contract.id),
|
||||||
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
fields: {
|
||||||
},
|
volume24Hours: computeVolume(contractBets, now - DAY_MS),
|
||||||
}
|
volume7Days: computeVolume(contractBets, now - DAY_MS * 7),
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
await writeAsync(firestore, contractUpdates)
|
await writeAsync(firestore, contractUpdates)
|
||||||
log(`Updated metrics for ${contracts.length} contracts.`)
|
log(`Updated metrics for ${contracts.length} contracts.`)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { chunk } from 'lodash'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { PrivateUser, User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
|
import { Post } from 'common/post'
|
||||||
|
|
||||||
export const log = (...args: unknown[]) => {
|
export const log = (...args: unknown[]) => {
|
||||||
console.log(`[${new Date().toISOString()}]`, ...args)
|
console.log(`[${new Date().toISOString()}]`, ...args)
|
||||||
|
@ -80,6 +81,10 @@ export const getGroup = (groupId: string) => {
|
||||||
return getDoc<Group>('groups', groupId)
|
return getDoc<Group>('groups', groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPost = (postId: string) => {
|
||||||
|
return getDoc<Post>('posts', postId)
|
||||||
|
}
|
||||||
|
|
||||||
export const getUser = (userId: string) => {
|
export const getUser = (userId: string) => {
|
||||||
return getDoc<User>('users', userId)
|
return getDoc<User>('users', userId)
|
||||||
}
|
}
|
||||||
|
|
10
package.json
10
package.json
|
@ -8,20 +8,22 @@
|
||||||
"web"
|
"web"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"verify": "(cd web && yarn verify:dir); (cd functions && yarn verify:dir)"
|
"verify": "(cd web && yarn verify:dir); (cd functions && yarn verify:dir)",
|
||||||
|
"lint": "eslint common --fix ; eslint web --fix ; eslint functions --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "16.11.11",
|
||||||
"@typescript-eslint/eslint-plugin": "5.25.0",
|
"@typescript-eslint/eslint-plugin": "5.25.0",
|
||||||
"@typescript-eslint/parser": "5.25.0",
|
"@typescript-eslint/parser": "5.25.0",
|
||||||
"@types/node": "16.11.11",
|
|
||||||
"concurrently": "6.5.1",
|
"concurrently": "6.5.1",
|
||||||
"eslint": "8.15.0",
|
"eslint": "8.15.0",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
|
"nodemon": "2.0.19",
|
||||||
"prettier": "2.5.0",
|
"prettier": "2.5.0",
|
||||||
"typescript": "4.6.4",
|
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"nodemon": "2.0.19"
|
"typescript": "4.6.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "17.0.43"
|
"@types/react": "17.0.43"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['lodash'],
|
plugins: ['lodash', 'unused-imports'],
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
|
@ -22,6 +22,7 @@ module.exports = {
|
||||||
'@next/next/no-typos': 'off',
|
'@next/next/no-typos': 'off',
|
||||||
'linebreak-style': ['error', 'unix'],
|
'linebreak-style': ['error', 'unix'],
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState, ReactNode } from 'react'
|
|
||||||
|
|
||||||
export function AdvancedPanel(props: { children: ReactNode }) {
|
|
||||||
const { children } = props
|
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
className={clsx(
|
|
||||||
'collapse collapse-arrow relative',
|
|
||||||
collapsed ? 'collapse-close' : 'collapse-open'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => setCollapsed((collapsed) => !collapsed)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="mt-4 mr-6 text-right text-sm text-gray-500">
|
|
||||||
Advanced
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="collapse-title absolute h-0 min-h-0 w-0 p-0"
|
|
||||||
style={{
|
|
||||||
top: -2,
|
|
||||||
right: -15,
|
|
||||||
color: '#6a7280' /* gray-500 */,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="collapse-content m-0 !bg-transparent !p-0">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Point, ResponsiveLine } from '@nivo/line'
|
import { Point, ResponsiveLine } from '@nivo/line'
|
||||||
|
import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { zip } from 'lodash'
|
import { zip } from 'lodash'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
@ -26,8 +27,10 @@ export function DailyCountChart(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-hidden"
|
className={clsx(
|
||||||
style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
|
'h-[250px] w-full overflow-hidden',
|
||||||
|
!small && 'md:h-[400px]'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<ResponsiveLine
|
<ResponsiveLine
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -78,8 +81,10 @@ export function DailyPercentChart(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-hidden"
|
className={clsx(
|
||||||
style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
|
'h-[250px] w-full overflow-hidden',
|
||||||
|
!small && 'md:h-[400px]'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<ResponsiveLine
|
<ResponsiveLine
|
||||||
data={data}
|
data={data}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
} from 'common/calculate-dpm'
|
} from 'common/calculate-dpm'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { SignUpPrompt } from '../sign-up-prompt'
|
import { BetSignUpPrompt } from '../sign-up-prompt'
|
||||||
import { isIOS } from 'web/lib/util/device'
|
import { isIOS } from 'web/lib/util/device'
|
||||||
import { AlertBox } from '../alert-box'
|
import { AlertBox } from '../alert-box'
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ export function AnswerBetPanel(props: {
|
||||||
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
{isSubmitting ? 'Submitting...' : 'Submit trade'}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<SignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -71,10 +71,11 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||||
const yTickValues = [0, 25, 50, 75, 100]
|
const yTickValues = [0, 25, 50, 75, 100]
|
||||||
|
|
||||||
const numXTickValues = isLargeWidth ? 5 : 2
|
const numXTickValues = isLargeWidth ? 5 : 2
|
||||||
const hoursAgo = latestTime.subtract(5, 'hours')
|
const startDate = new Date(contract.createdTime)
|
||||||
const startDate = dayjs(contract.createdTime).isBefore(hoursAgo)
|
const endDate = dayjs(startDate).add(1, 'hour').isAfter(latestTime)
|
||||||
? new Date(contract.createdTime)
|
? latestTime.add(1, 'hours').toDate()
|
||||||
: hoursAgo.toDate()
|
: latestTime.toDate()
|
||||||
|
const includeMinute = dayjs(endDate).diff(startDate, 'hours') < 2
|
||||||
|
|
||||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
||||||
const lessThanAWeek = dayjs(startDate).add(1, 'week').isAfter(latestTime)
|
const lessThanAWeek = dayjs(startDate).add(1, 'week').isAfter(latestTime)
|
||||||
|
@ -96,16 +97,24 @@ export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||||
xScale={{
|
xScale={{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
min: startDate,
|
min: startDate,
|
||||||
max: latestTime.toDate(),
|
max: endDate,
|
||||||
}}
|
}}
|
||||||
xFormat={(d) =>
|
xFormat={(d) =>
|
||||||
formatTime(+d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
formatTime(+d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
||||||
}
|
}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
tickValues: numXTickValues,
|
tickValues: numXTickValues,
|
||||||
format: (time) => formatTime(+time, multiYear, lessThanAWeek, false),
|
format: (time) =>
|
||||||
|
formatTime(+time, multiYear, lessThanAWeek, includeMinute),
|
||||||
}}
|
}}
|
||||||
colors={{ scheme: 'pastel1' }}
|
colors={[
|
||||||
|
'#fca5a5', // red-300
|
||||||
|
'#a5b4fc', // indigo-300
|
||||||
|
'#86efac', // green-300
|
||||||
|
'#fef08a', // yellow-200
|
||||||
|
'#fdba74', // orange-300
|
||||||
|
'#c084fc', // purple-400
|
||||||
|
]}
|
||||||
pointSize={0}
|
pointSize={0}
|
||||||
curve="stepAfter"
|
curve="stepAfter"
|
||||||
enableSlices="x"
|
enableSlices="x"
|
||||||
|
@ -156,7 +165,11 @@ function formatTime(
|
||||||
) {
|
) {
|
||||||
const d = dayjs(time)
|
const d = dayjs(time)
|
||||||
|
|
||||||
if (d.add(1, 'minute').isAfter(Date.now())) return 'Now'
|
if (
|
||||||
|
d.add(1, 'minute').isAfter(Date.now()) &&
|
||||||
|
d.subtract(1, 'minute').isBefore(Date.now())
|
||||||
|
)
|
||||||
|
return 'Now'
|
||||||
|
|
||||||
let format: string
|
let format: string
|
||||||
if (d.isSame(Date.now(), 'day')) {
|
if (d.isSame(Date.now(), 'day')) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { Bet } from 'common/bet'
|
||||||
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { lowerCase } from 'lodash'
|
import { lowerCase } from 'lodash'
|
||||||
|
import { Button } from '../button'
|
||||||
|
|
||||||
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -115,6 +116,8 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||||
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
|
||||||
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
|
||||||
|
|
||||||
|
if (user?.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="gap-4 rounded">
|
<Col className="gap-4 rounded">
|
||||||
<Col className="flex-1 gap-2">
|
<Col className="flex-1 gap-2">
|
||||||
|
@ -201,12 +204,14 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
text && (
|
text && (
|
||||||
<button
|
<Button
|
||||||
className="btn self-end 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"
|
color="green"
|
||||||
|
size="lg"
|
||||||
|
className="self-end whitespace-nowrap "
|
||||||
onClick={withTracking(firebaseLogin, 'answer panel sign in')}
|
onClick={withTracking(firebaseLogin, 'answer panel sign in')}
|
||||||
>
|
>
|
||||||
Sign in
|
Add my answer
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -19,6 +19,15 @@ import { useStateCheckEquality } from 'web/hooks/use-state-check-equality'
|
||||||
type AuthUser = undefined | null | UserAndPrivateUser
|
type AuthUser = undefined | null | UserAndPrivateUser
|
||||||
|
|
||||||
const CACHED_USER_KEY = 'CACHED_USER_KEY_V2'
|
const CACHED_USER_KEY = 'CACHED_USER_KEY_V2'
|
||||||
|
// Proxy localStorage in case it's not available (eg in incognito iframe)
|
||||||
|
const localStorage =
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? window.localStorage
|
||||||
|
: {
|
||||||
|
getItem: () => null,
|
||||||
|
setItem: () => {},
|
||||||
|
removeItem: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
const ensureDeviceToken = () => {
|
const ensureDeviceToken = () => {
|
||||||
let deviceToken = localStorage.getItem('device-token')
|
let deviceToken = localStorage.getItem('device-token')
|
||||||
|
@ -46,29 +55,35 @@ export function AuthProvider(props: {
|
||||||
}, [setAuthUser, serverUser])
|
}, [setAuthUser, serverUser])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return onIdTokenChanged(auth, async (fbUser) => {
|
return onIdTokenChanged(
|
||||||
if (fbUser) {
|
auth,
|
||||||
setTokenCookies({
|
async (fbUser) => {
|
||||||
id: await fbUser.getIdToken(),
|
if (fbUser) {
|
||||||
refresh: fbUser.refreshToken,
|
setTokenCookies({
|
||||||
})
|
id: await fbUser.getIdToken(),
|
||||||
let current = await getUserAndPrivateUser(fbUser.uid)
|
refresh: fbUser.refreshToken,
|
||||||
if (!current.user || !current.privateUser) {
|
})
|
||||||
const deviceToken = ensureDeviceToken()
|
let current = await getUserAndPrivateUser(fbUser.uid)
|
||||||
current = (await createUser({ deviceToken })) as UserAndPrivateUser
|
if (!current.user || !current.privateUser) {
|
||||||
|
const deviceToken = ensureDeviceToken()
|
||||||
|
current = (await createUser({ deviceToken })) as UserAndPrivateUser
|
||||||
|
}
|
||||||
|
setAuthUser(current)
|
||||||
|
// Persist to local storage, to reduce login blink next time.
|
||||||
|
// Note: Cap on localStorage size is ~5mb
|
||||||
|
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(current))
|
||||||
|
setCachedReferralInfoForUser(current.user)
|
||||||
|
} else {
|
||||||
|
// User logged out; reset to null
|
||||||
|
deleteTokenCookies()
|
||||||
|
setAuthUser(null)
|
||||||
|
localStorage.removeItem(CACHED_USER_KEY)
|
||||||
}
|
}
|
||||||
setAuthUser(current)
|
},
|
||||||
// Persist to local storage, to reduce login blink next time.
|
(e) => {
|
||||||
// Note: Cap on localStorage size is ~5mb
|
console.error(e)
|
||||||
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(current))
|
|
||||||
setCachedReferralInfoForUser(current.user)
|
|
||||||
} else {
|
|
||||||
// User logged out; reset to null
|
|
||||||
deleteTokenCookies()
|
|
||||||
setAuthUser(null)
|
|
||||||
localStorage.removeItem(CACHED_USER_KEY)
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}, [setAuthUser])
|
}, [setAuthUser])
|
||||||
|
|
||||||
const uid = authUser?.user.id
|
const uid = authUser?.user.id
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { useSaveBinaryShares } from './use-save-binary-shares'
|
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
|
|
||||||
/** Button that opens BetPanel in a new modal */
|
/** Button that opens BetPanel in a new modal */
|
||||||
export default function BetButton(props: {
|
export default function BetButton(props: {
|
||||||
|
@ -32,15 +32,17 @@ export default function BetButton(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col className={clsx('items-center', className)}>
|
<Col className={clsx('items-center', className)}>
|
||||||
<Button
|
{user ? (
|
||||||
size={'lg'}
|
<Button
|
||||||
className={clsx('my-auto inline-flex min-w-[75px] ', btnClassName)}
|
size="lg"
|
||||||
onClick={() => {
|
className={clsx('my-auto inline-flex min-w-[75px] ', btnClassName)}
|
||||||
!user ? firebaseLogin() : setOpen(true)
|
onClick={() => setOpen(true)}
|
||||||
}}
|
>
|
||||||
>
|
Bet
|
||||||
{user ? 'Bet' : 'Sign up to Bet'}
|
</Button>
|
||||||
</Button>
|
) : (
|
||||||
|
<BetSignUpPrompt />
|
||||||
|
)}
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Row } from './layout/row'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { SignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { getCpmmProbability } from 'common/calculate-cpmm'
|
import { getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { XIcon } from '@heroicons/react/solid'
|
import { XIcon } from '@heroicons/react/solid'
|
||||||
|
@ -112,7 +112,7 @@ export function BetInline(props: {
|
||||||
: 'Submit'}
|
: 'Submit'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<SignUpPrompt size="xs" />
|
<BetSignUpPrompt size="xs" />
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setProbAfter(undefined)
|
setProbAfter(undefined)
|
||||||
|
|
|
@ -31,7 +31,7 @@ import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
||||||
import { SellRow } from './sell-row'
|
import { SellRow } from './sell-row'
|
||||||
import { useSaveBinaryShares } from './use-save-binary-shares'
|
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||||
import { SignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { isIOS } from 'web/lib/util/device'
|
import { isIOS } from 'web/lib/util/device'
|
||||||
import { ProbabilityOrNumericInput } from './probability-input'
|
import { ProbabilityOrNumericInput } from './probability-input'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
@ -86,7 +86,7 @@ export function BetPanel(props: {
|
||||||
unfilledBets={unfilledBets}
|
unfilledBets={unfilledBets}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
|
|
||||||
{!user && <PlayMoneyDisclaimer />}
|
{!user && <PlayMoneyDisclaimer />}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -146,7 +146,7 @@ export function SimpleBetPanel(props: {
|
||||||
onBuySuccess={onBetSuccess}
|
onBuySuccess={onBetSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
|
|
||||||
{!user && <PlayMoneyDisclaimer />}
|
{!user && <PlayMoneyDisclaimer />}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -560,7 +560,7 @@ function LimitOrderPanel(props: {
|
||||||
<Row className="mt-1 items-center gap-4">
|
<Row className="mt-1 items-center gap-4">
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<div className="relative ml-1 text-sm text-gray-500">
|
<div className="relative ml-1 text-sm text-gray-500">
|
||||||
Bet {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} at
|
Bet {isPseudoNumeric ? <HigherLabel /> : <YesLabel />} up to
|
||||||
</div>
|
</div>
|
||||||
<ProbabilityOrNumericInput
|
<ProbabilityOrNumericInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
@ -571,7 +571,7 @@ function LimitOrderPanel(props: {
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<div className="ml-1 text-sm text-gray-500">
|
<div className="ml-1 text-sm text-gray-500">
|
||||||
Bet {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} at
|
Bet {isPseudoNumeric ? <LowerLabel /> : <NoLabel />} down to
|
||||||
</div>
|
</div>
|
||||||
<ProbabilityOrNumericInput
|
<ProbabilityOrNumericInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
|
@ -1,14 +1,5 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {
|
import { keyBy, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash'
|
||||||
Dictionary,
|
|
||||||
keyBy,
|
|
||||||
groupBy,
|
|
||||||
mapValues,
|
|
||||||
sortBy,
|
|
||||||
partition,
|
|
||||||
sumBy,
|
|
||||||
uniq,
|
|
||||||
} from 'lodash'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
@ -28,7 +19,6 @@ import {
|
||||||
Contract,
|
Contract,
|
||||||
contractPath,
|
contractPath,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
getContractFromId,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { sellBet } from 'web/lib/firebase/api'
|
import { sellBet } from 'web/lib/firebase/api'
|
||||||
|
@ -55,10 +45,10 @@ import { SellSharesModal } from './sell-modal'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
import { LimitBet } from 'common/bet'
|
import { LimitBet } from 'common/bet'
|
||||||
import { floatingEqual } from 'common/util/math'
|
import { floatingEqual } from 'common/util/math'
|
||||||
import { filterDefined } from 'common/util/array'
|
|
||||||
import { Pagination } from './pagination'
|
import { Pagination } from './pagination'
|
||||||
import { LimitOrderTable } from './limit-bets'
|
import { LimitOrderTable } from './limit-bets'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import { useUserBetContracts } from 'web/hooks/use-contracts'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -72,26 +62,22 @@ export function BetsList(props: { user: User }) {
|
||||||
const signedInUser = useUser()
|
const signedInUser = useUser()
|
||||||
const isYourBets = user.id === signedInUser?.id
|
const isYourBets = user.id === signedInUser?.id
|
||||||
const hideBetsBefore = isYourBets ? 0 : JUNE_1_2022
|
const hideBetsBefore = isYourBets ? 0 : JUNE_1_2022
|
||||||
const userBets = useUserBets(user.id, { includeRedemptions: true })
|
const userBets = useUserBets(user.id)
|
||||||
const [contractsById, setContractsById] = useState<
|
|
||||||
Dictionary<Contract> | undefined
|
|
||||||
>()
|
|
||||||
|
|
||||||
// Hide bets before 06-01-2022 if this isn't your own profile
|
// Hide bets before 06-01-2022 if this isn't your own profile
|
||||||
// NOTE: This means public profits also begin on 06-01-2022 as well.
|
// NOTE: This means public profits also begin on 06-01-2022 as well.
|
||||||
const bets = useMemo(
|
const bets = useMemo(
|
||||||
() => userBets?.filter((bet) => bet.createdTime >= (hideBetsBefore ?? 0)),
|
() =>
|
||||||
|
userBets?.filter(
|
||||||
|
(bet) => !bet.isAnte && bet.createdTime >= (hideBetsBefore ?? 0)
|
||||||
|
),
|
||||||
[userBets, hideBetsBefore]
|
[userBets, hideBetsBefore]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const contractList = useUserBetContracts(user.id)
|
||||||
if (bets) {
|
const contractsById = useMemo(() => {
|
||||||
const contractIds = uniq(bets.map((b) => b.contractId))
|
return contractList ? keyBy(contractList, 'id') : undefined
|
||||||
Promise.all(contractIds.map(getContractFromId)).then((contracts) => {
|
}, [contractList])
|
||||||
setContractsById(keyBy(filterDefined(contracts), 'id'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [bets])
|
|
||||||
|
|
||||||
const [sort, setSort] = useState<BetSort>('newest')
|
const [sort, setSort] = useState<BetSort>('newest')
|
||||||
const [filter, setFilter] = useState<BetFilter>('open')
|
const [filter, setFilter] = useState<BetFilter>('open')
|
||||||
|
@ -405,7 +391,8 @@ export function BetsSummary(props: {
|
||||||
const isClosed = closeTime && Date.now() > closeTime
|
const isClosed = closeTime && Date.now() > closeTime
|
||||||
|
|
||||||
const bets = props.bets.filter((b) => !b.isAnte)
|
const bets = props.bets.filter((b) => !b.isAnte)
|
||||||
const { hasShares } = getContractBetMetrics(contract, bets)
|
const { hasShares, invested, profitPercent, payout, profit, totalShares } =
|
||||||
|
getContractBetMetrics(contract, bets)
|
||||||
|
|
||||||
const excludeSalesAndAntes = bets.filter(
|
const excludeSalesAndAntes = bets.filter(
|
||||||
(b) => !b.isAnte && !b.isSold && !b.sale
|
(b) => !b.isAnte && !b.isSold && !b.sale
|
||||||
|
@ -416,8 +403,6 @@ export function BetsSummary(props: {
|
||||||
const noWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
const noWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
||||||
calculatePayout(contract, bet, 'NO')
|
calculatePayout(contract, bet, 'NO')
|
||||||
)
|
)
|
||||||
const { invested, profitPercent, payout, profit, totalShares } =
|
|
||||||
getContractBetMetrics(contract, bets)
|
|
||||||
|
|
||||||
const [showSellModal, setShowSellModal] = useState(false)
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -520,7 +505,7 @@ export function BetsSummary(props: {
|
||||||
) : (
|
) : (
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
Current value
|
Expected value
|
||||||
</div>
|
</div>
|
||||||
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
|
<div className="whitespace-nowrap">{formatMoney(payout)}</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -37,8 +37,8 @@ export function Button(props: {
|
||||||
sm: 'px-3 py-2 text-sm',
|
sm: 'px-3 py-2 text-sm',
|
||||||
md: 'px-4 py-2 text-sm',
|
md: 'px-4 py-2 text-sm',
|
||||||
lg: 'px-4 py-2 text-base',
|
lg: 'px-4 py-2 text-base',
|
||||||
xl: 'px-6 py-3 text-base',
|
xl: 'px-6 py-2.5 text-base font-semibold',
|
||||||
'2xl': 'px-6 py-3 text-xl',
|
'2xl': 'px-6 py-3 text-xl font-semibold',
|
||||||
}[size]
|
}[size]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -52,9 +52,9 @@ export function Button(props: {
|
||||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||||
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
|
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
|
||||||
color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
color === 'gray' && 'bg-gray-50 text-gray-600 hover:bg-gray-200',
|
||||||
color === 'gradient' &&
|
color === 'gradient' &&
|
||||||
'bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700',
|
'border-none bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700',
|
||||||
color === 'gray-white' &&
|
color === 'gray-white' &&
|
||||||
'border-none bg-white text-gray-500 shadow-none hover:bg-gray-200',
|
'border-none bg-white text-gray-500 shadow-none hover:bg-gray-200',
|
||||||
className
|
className
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { User } from 'common/user'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge } from 'common/challenge'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { SignUpPrompt } from 'web/components/sign-up-prompt'
|
import { BetSignUpPrompt } from 'web/components/sign-up-prompt'
|
||||||
import { acceptChallenge, APIError } from 'web/lib/firebase/api'
|
import { acceptChallenge, APIError } from 'web/lib/firebase/api'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
@ -27,7 +27,7 @@ export function AcceptChallengeButton(props: {
|
||||||
setErrorText('')
|
setErrorText('')
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
if (!user) return <SignUpPrompt label="Accept this bet" className="mt-4" />
|
if (!user) return <BetSignUpPrompt label="Accept this bet" className="mt-4" />
|
||||||
|
|
||||||
const iAcceptChallenge = () => {
|
const iAcceptChallenge = () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { ContractComment } from 'common/comment'
|
||||||
|
|
||||||
import { Comment, ContractComment } from 'common/comment'
|
|
||||||
import { groupConsecutive } from 'common/util/array'
|
import { groupConsecutive } from 'common/util/array'
|
||||||
import { getUsersComments } from 'web/lib/firebase/comments'
|
import { getUserCommentsQuery } from 'web/lib/firebase/comments'
|
||||||
|
import { usePagination } from 'web/hooks/use-pagination'
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Avatar } from './avatar'
|
import { Avatar } from './avatar'
|
||||||
|
@ -10,11 +9,15 @@ import { RelativeTimestamp } from './relative-timestamp'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Content } from './editor'
|
import { Content } from './editor'
|
||||||
import { Pagination } from './pagination'
|
|
||||||
import { LoadingIndicator } from './loading-indicator'
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import { PaginationNextPrev } from 'web/components/pagination'
|
||||||
|
|
||||||
const COMMENTS_PER_PAGE = 50
|
type ContractKey = {
|
||||||
|
contractId: string
|
||||||
|
contractSlug: string
|
||||||
|
contractQuestion: string
|
||||||
|
}
|
||||||
|
|
||||||
function contractPath(slug: string) {
|
function contractPath(slug: string) {
|
||||||
// by convention this includes the contract creator username, but we don't
|
// by convention this includes the contract creator username, but we don't
|
||||||
|
@ -24,67 +27,83 @@ function contractPath(slug: string) {
|
||||||
|
|
||||||
export function UserCommentsList(props: { user: User }) {
|
export function UserCommentsList(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
const [comments, setComments] = useState<ContractComment[] | undefined>()
|
|
||||||
const [page, setPage] = useState(0)
|
|
||||||
const start = page * COMMENTS_PER_PAGE
|
|
||||||
const end = start + COMMENTS_PER_PAGE
|
|
||||||
|
|
||||||
useEffect(() => {
|
const page = usePagination({ q: getUserCommentsQuery(user.id), pageSize: 50 })
|
||||||
getUsersComments(user.id).then((cs) => {
|
const { isStart, isEnd, getNext, getPrev, getItems, isLoading } = page
|
||||||
// we don't show comments in groups here atm, just comments on contracts
|
|
||||||
setComments(
|
|
||||||
cs.filter((c) => c.commentType == 'contract') as ContractComment[]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [user.id])
|
|
||||||
|
|
||||||
if (comments == null) {
|
const pageComments = groupConsecutive(getItems(), (c) => {
|
||||||
|
return {
|
||||||
|
contractId: c.contractId,
|
||||||
|
contractQuestion: c.contractQuestion,
|
||||||
|
contractSlug: c.contractSlug,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageComments = groupConsecutive(comments.slice(start, end), (c) => {
|
if (pageComments.length === 0) {
|
||||||
return { question: c.contractQuestion, slug: c.contractSlug }
|
if (isStart && isEnd) {
|
||||||
})
|
return <p>This user hasn't made any comments yet.</p>
|
||||||
|
} else {
|
||||||
|
// this can happen if their comment count is a multiple of page size
|
||||||
|
return <p>No more comments to display.</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'bg-white'}>
|
<Col className={'bg-white'}>
|
||||||
{pageComments.map(({ key, items }, i) => {
|
{pageComments.map(({ key, items }, i) => {
|
||||||
return (
|
return <ProfileCommentGroup key={i} groupKey={key} items={items} />
|
||||||
<div key={start + i} className="border-b p-5">
|
|
||||||
<SiteLink
|
|
||||||
className="mb-2 block pb-2 font-medium text-indigo-700"
|
|
||||||
href={contractPath(key.slug)}
|
|
||||||
>
|
|
||||||
{key.question}
|
|
||||||
</SiteLink>
|
|
||||||
<Col className="gap-6">
|
|
||||||
{items.map((comment) => (
|
|
||||||
<ProfileComment
|
|
||||||
key={comment.id}
|
|
||||||
comment={comment}
|
|
||||||
className="relative flex items-start space-x-3"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Col>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
<Pagination
|
<nav
|
||||||
page={page}
|
className="border-t border-gray-200 px-4 py-3 sm:px-6"
|
||||||
itemsPerPage={COMMENTS_PER_PAGE}
|
aria-label="Pagination"
|
||||||
totalItems={comments.length}
|
>
|
||||||
setPage={setPage}
|
<PaginationNextPrev
|
||||||
/>
|
prev={!isStart ? 'Previous' : null}
|
||||||
|
next={!isEnd ? 'Next' : null}
|
||||||
|
onClickPrev={getPrev}
|
||||||
|
onClickNext={getNext}
|
||||||
|
scrollToTop={true}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileComment(props: { comment: Comment; className?: string }) {
|
function ProfileCommentGroup(props: {
|
||||||
const { comment, className } = props
|
groupKey: ContractKey
|
||||||
|
items: ContractComment[]
|
||||||
|
}) {
|
||||||
|
const { groupKey, items } = props
|
||||||
|
const { contractSlug, contractQuestion } = groupKey
|
||||||
|
const path = contractPath(contractSlug)
|
||||||
|
return (
|
||||||
|
<div className="border-b p-5">
|
||||||
|
<SiteLink
|
||||||
|
className="mb-2 block pb-2 font-medium text-indigo-700"
|
||||||
|
href={path}
|
||||||
|
>
|
||||||
|
{contractQuestion}
|
||||||
|
</SiteLink>
|
||||||
|
<Col className="gap-6">
|
||||||
|
{items.map((c) => (
|
||||||
|
<ProfileComment key={c.id} comment={c} />
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileComment(props: { comment: ContractComment }) {
|
||||||
|
const { comment } = props
|
||||||
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
||||||
comment
|
comment
|
||||||
// TODO: find and attach relevant bets by comment betId at some point
|
// TODO: find and attach relevant bets by comment betId at some point
|
||||||
return (
|
return (
|
||||||
<Row className={className}>
|
<Row className="relative flex items-start space-x-3">
|
||||||
<Avatar username={userUsername} avatarUrl={userAvatarUrl} />
|
<Avatar username={userUsername} avatarUrl={userAvatarUrl} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="mt-0.5 text-sm text-gray-500">
|
<p className="mt-0.5 text-sm text-gray-500">
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { SparklesIcon } from '@heroicons/react/solid'
|
|
||||||
|
|
||||||
export function FeaturedContractBadge() {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-3 py-0.5 text-sm font-medium text-blue-800">
|
|
||||||
<SparklesIcon className="h-4 w-4" aria-hidden="true" /> Featured
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -32,23 +32,28 @@ import { track } from '@amplitude/analytics-browser'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
import { getMappedValue } from 'common/pseudo-numeric'
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
import { Tooltip } from '../tooltip'
|
import { Tooltip } from '../tooltip'
|
||||||
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
showHotVolume?: boolean
|
showHotVolume?: boolean
|
||||||
showTime?: ShowTime
|
showTime?: ShowTime
|
||||||
className?: string
|
className?: string
|
||||||
|
questionClass?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
|
trackingPostfix?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
showHotVolume,
|
showHotVolume,
|
||||||
showTime,
|
showTime,
|
||||||
className,
|
className,
|
||||||
|
questionClass,
|
||||||
onClick,
|
onClick,
|
||||||
hideQuickBet,
|
hideQuickBet,
|
||||||
hideGroupLink,
|
hideGroupLink,
|
||||||
|
trackingPostfix,
|
||||||
} = props
|
} = props
|
||||||
const contract = useContractWithPreload(props.contract) ?? props.contract
|
const contract = useContractWithPreload(props.contract) ?? props.contract
|
||||||
const { question, outcomeType } = contract
|
const { question, outcomeType } = contract
|
||||||
|
@ -59,7 +64,11 @@ export function ContractCard(props: {
|
||||||
const marketClosed =
|
const marketClosed =
|
||||||
(contract.closeTime || Infinity) < Date.now() || !!resolution
|
(contract.closeTime || Infinity) < Date.now() || !!resolution
|
||||||
|
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = (width ?? 0) < 768
|
||||||
|
|
||||||
const showQuickBet =
|
const showQuickBet =
|
||||||
|
!isMobile &&
|
||||||
user &&
|
user &&
|
||||||
!marketClosed &&
|
!marketClosed &&
|
||||||
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') &&
|
||||||
|
@ -68,45 +77,20 @@ export function ContractCard(props: {
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative gap-3 self-start rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
'group relative gap-3 rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Col className="group relative flex-1 gap-3 py-4 pb-12 pl-6">
|
<Col className="relative flex-1 gap-3 py-4 pb-12 pl-6">
|
||||||
{onClick ? (
|
|
||||||
<a
|
|
||||||
className="absolute top-0 left-0 right-0 bottom-0"
|
|
||||||
href={contractPath(contract)}
|
|
||||||
onClick={(e) => {
|
|
||||||
// Let the browser handle the link click (opens in new tab).
|
|
||||||
if (e.ctrlKey || e.metaKey) return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
track('click market card', {
|
|
||||||
slug: contract.slug,
|
|
||||||
contractId: contract.id,
|
|
||||||
})
|
|
||||||
onClick()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Link href={contractPath(contract)}>
|
|
||||||
<a
|
|
||||||
onClick={trackCallback('click market card', {
|
|
||||||
slug: contract.slug,
|
|
||||||
contractId: contract.id,
|
|
||||||
})}
|
|
||||||
className="absolute top-0 left-0 right-0 bottom-0"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<AvatarDetails
|
<AvatarDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
className={'hidden md:inline-flex'}
|
className={'hidden md:inline-flex'}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className="break-words font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2"
|
className={clsx(
|
||||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
'break-anywhere font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2',
|
||||||
|
questionClass
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
</p>
|
</p>
|
||||||
|
@ -124,7 +108,7 @@ export function ContractCard(props: {
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
{showQuickBet ? (
|
{showQuickBet ? (
|
||||||
<QuickBet contract={contract} user={user} />
|
<QuickBet contract={contract} user={user} className="z-10" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'BINARY' && (
|
||||||
|
@ -165,11 +149,7 @@ export function ContractCard(props: {
|
||||||
showQuickBet ? 'w-[85%]' : 'w-full'
|
showQuickBet ? 'w-[85%]' : 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AvatarDetails
|
<AvatarDetails contract={contract} short={true} className="md:hidden" />
|
||||||
contract={contract}
|
|
||||||
short={true}
|
|
||||||
className={'block md:hidden'}
|
|
||||||
/>
|
|
||||||
<MiscDetails
|
<MiscDetails
|
||||||
contract={contract}
|
contract={contract}
|
||||||
showHotVolume={showHotVolume}
|
showHotVolume={showHotVolume}
|
||||||
|
@ -177,6 +157,38 @@ export function ContractCard(props: {
|
||||||
hideGroupLink={hideGroupLink}
|
hideGroupLink={hideGroupLink}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{/* Add click layer */}
|
||||||
|
{onClick ? (
|
||||||
|
<a
|
||||||
|
className="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
href={contractPath(contract)}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Let the browser handle the link click (opens in new tab).
|
||||||
|
if (e.ctrlKey || e.metaKey) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
track('click market card' + (trackingPostfix ?? ''), {
|
||||||
|
slug: contract.slug,
|
||||||
|
contractId: contract.id,
|
||||||
|
})
|
||||||
|
onClick()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Link href={contractPath(contract)}>
|
||||||
|
<a
|
||||||
|
onClick={trackCallback(
|
||||||
|
'click market card' + (trackingPostfix ?? ''),
|
||||||
|
{
|
||||||
|
slug: contract.slug,
|
||||||
|
contractId: contract.id,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
className="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,7 @@ function EditQuestion(props: {
|
||||||
|
|
||||||
function joinContent(oldContent: ContentType, newContent: string) {
|
function joinContent(oldContent: ContentType, newContent: string) {
|
||||||
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
const editor = new Editor({ content: oldContent, extensions: exhibitExts })
|
||||||
|
editor.commands.focus('end')
|
||||||
insertContent(editor, newContent)
|
insertContent(editor, newContent)
|
||||||
return editor.getJSON()
|
return editor.getJSON()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,14 @@ import {
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
UserGroupIcon,
|
UserGroupIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Editor } from '@tiptap/react'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
|
@ -20,7 +23,6 @@ import NewContractBadge from '../new-contract-badge'
|
||||||
import { UserFollowButton } from '../follow-button'
|
import { UserFollowButton } from '../follow-button'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { Editor } from '@tiptap/react'
|
|
||||||
import { exhibitExts } from 'common/util/parse'
|
import { exhibitExts } from 'common/util/parse'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
@ -29,11 +31,10 @@ import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { insertContent } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { contractMetrics } from 'common/contract-details'
|
import { contractMetrics } from 'common/contract-details'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { FeaturedContractBadge } from 'web/components/contract/FeaturedContractBadge'
|
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -187,14 +188,29 @@ export function ContractDetails(props: {
|
||||||
) : !groupToDisplay && !user ? (
|
) : !groupToDisplay && !user ? (
|
||||||
<div />
|
<div />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Row>
|
||||||
size={'xs'}
|
<Button
|
||||||
className={'max-w-[200px]'}
|
size={'xs'}
|
||||||
color={'gray-white'}
|
className={'max-w-[200px] pr-2'}
|
||||||
onClick={() => setOpen(!open)}
|
color={'gray-white'}
|
||||||
>
|
onClick={() =>
|
||||||
{groupInfo}
|
groupToDisplay
|
||||||
</Button>
|
? Router.push(groupPath(groupToDisplay.slug))
|
||||||
|
: setOpen(!open)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groupInfo}
|
||||||
|
</Button>
|
||||||
|
{user && (
|
||||||
|
<Button
|
||||||
|
size={'xs'}
|
||||||
|
color={'gray-white'}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="mb-0.5 mr-0.5 inline h-4 w-4 shrink-0" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Modal open={open} setOpen={setOpen} size={'md'}>
|
<Modal open={open} setOpen={setOpen} size={'md'}>
|
||||||
|
@ -218,7 +234,7 @@ export function ContractDetails(props: {
|
||||||
<ClockIcon className="h-5 w-5" />
|
<ClockIcon className="h-5 w-5" />
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip
|
||||||
text="Market resolved:"
|
text="Market resolved:"
|
||||||
time={dayjs(contract.resolutionTime)}
|
time={contract.resolutionTime}
|
||||||
>
|
>
|
||||||
{resolvedDate}
|
{resolvedDate}
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
|
@ -262,14 +278,22 @@ function EditableCloseDate(props: {
|
||||||
|
|
||||||
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
||||||
const [closeDate, setCloseDate] = useState(
|
const [closeDate, setCloseDate] = useState(
|
||||||
closeTime && dayJsCloseTime.format('YYYY-MM-DDTHH:mm')
|
closeTime && dayJsCloseTime.format('YYYY-MM-DD')
|
||||||
)
|
)
|
||||||
|
const [closeHoursMinutes, setCloseHoursMinutes] = useState(
|
||||||
|
closeTime && dayJsCloseTime.format('HH:mm')
|
||||||
|
)
|
||||||
|
|
||||||
|
const newCloseTime = closeDate
|
||||||
|
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
|
||||||
|
: undefined
|
||||||
|
|
||||||
const isSameYear = dayJsCloseTime.isSame(dayJsNow, 'year')
|
const isSameYear = dayJsCloseTime.isSame(dayJsNow, 'year')
|
||||||
const isSameDay = dayJsCloseTime.isSame(dayJsNow, 'day')
|
const isSameDay = dayJsCloseTime.isSame(dayJsNow, 'day')
|
||||||
|
|
||||||
const onSave = () => {
|
const onSave = () => {
|
||||||
const newCloseTime = dayjs(closeDate).valueOf()
|
if (!newCloseTime) return
|
||||||
|
|
||||||
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
||||||
else if (newCloseTime > Date.now()) {
|
else if (newCloseTime > Date.now()) {
|
||||||
const content = contract.description
|
const content = contract.description
|
||||||
|
@ -294,20 +318,28 @@ function EditableCloseDate(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEditingCloseTime ? (
|
{isEditingCloseTime ? (
|
||||||
<div className="form-control mr-1 items-start">
|
<Row className="mr-1 items-start">
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="date"
|
||||||
className="input input-bordered"
|
className="input input-bordered"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setCloseDate(e.target.value || '')}
|
onChange={(e) => setCloseDate(e.target.value)}
|
||||||
min={Date.now()}
|
min={Date.now()}
|
||||||
value={closeDate}
|
value={closeDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
<input
|
||||||
|
type="time"
|
||||||
|
className="input input-bordered ml-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
||||||
|
min="00:00"
|
||||||
|
value={closeHoursMinutes}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip
|
||||||
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
||||||
time={dayJsCloseTime}
|
time={closeTime}
|
||||||
>
|
>
|
||||||
{isSameYear
|
{isSameYear
|
||||||
? dayJsCloseTime.format('MMM D')
|
? dayJsCloseTime.format('MMM D')
|
||||||
|
@ -327,7 +359,7 @@ function EditableCloseDate(props: {
|
||||||
color={'gray-white'}
|
color={'gray-white'}
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
onClick={() => setIsEditingCloseTime(true)}
|
||||||
>
|
>
|
||||||
<PencilIcon className="mr-0.5 inline h-4 w-4" /> Edit
|
<PencilIcon className="!container mr-0.5 mb-0.5 inline h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useAdmin, useDev } from 'web/hooks/use-admin'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { firestoreConsolePath } from 'common/envs/constants'
|
import { firestoreConsolePath } from 'common/envs/constants'
|
||||||
import { deleteField } from 'firebase/firestore'
|
import { deleteField } from 'firebase/firestore'
|
||||||
|
import ShortToggle from '../widgets/short-toggle'
|
||||||
|
|
||||||
export const contractDetailsButtonClassName =
|
export const contractDetailsButtonClassName =
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||||
|
@ -31,7 +32,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const isDev = useDev()
|
const isDev = useDev()
|
||||||
const isAdmin = useAdmin()
|
const isAdmin = useAdmin()
|
||||||
|
|
||||||
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
|
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a')
|
||||||
|
|
||||||
const { createdTime, closeTime, resolutionTime, mechanism, outcomeType, id } =
|
const { createdTime, closeTime, resolutionTime, mechanism, outcomeType, id } =
|
||||||
contract
|
contract
|
||||||
|
@ -50,6 +51,21 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
? 'Multiple choice'
|
? 'Multiple choice'
|
||||||
: 'Numeric'
|
: 'Numeric'
|
||||||
|
|
||||||
|
const onFeaturedToggle = async (enabled: boolean) => {
|
||||||
|
if (
|
||||||
|
enabled &&
|
||||||
|
(contract.featuredOnHomeRank === 0 || !contract?.featuredOnHomeRank)
|
||||||
|
) {
|
||||||
|
await updateContract(id, { featuredOnHomeRank: 1 })
|
||||||
|
setFeatured(true)
|
||||||
|
} else if (!enabled && (contract?.featuredOnHomeRank ?? 0) > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
await updateContract(id, { featuredOnHomeRank: deleteField() })
|
||||||
|
setFeatured(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
@ -134,7 +150,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
{/* Show a path to Firebase if user is an admin, or we're on localhost */}
|
{/* Show a path to Firebase if user is an admin, or we're on localhost */}
|
||||||
{(isAdmin || isDev) && (
|
{(isAdmin || isDev) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>[DEV] Firestore</td>
|
<td>[ADMIN] Firestore</td>
|
||||||
<td>
|
<td>
|
||||||
<SiteLink href={firestoreConsolePath(id)}>
|
<SiteLink href={firestoreConsolePath(id)}>
|
||||||
Console link
|
Console link
|
||||||
|
@ -144,43 +160,28 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Set featured</td>
|
<td>[ADMIN] Featured</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<ShortToggle
|
||||||
className="select select-bordered"
|
enabled={featured}
|
||||||
value={featured ? 'true' : 'false'}
|
setEnabled={setFeatured}
|
||||||
onChange={(e) => {
|
onChange={onFeaturedToggle}
|
||||||
const newVal = e.target.value === 'true'
|
/>
|
||||||
if (
|
</td>
|
||||||
newVal &&
|
</tr>
|
||||||
(contract.featuredOnHomeRank === 0 ||
|
)}
|
||||||
!contract?.featuredOnHomeRank)
|
{isAdmin && (
|
||||||
)
|
<tr>
|
||||||
updateContract(id, {
|
<td>[ADMIN] Unlisted</td>
|
||||||
featuredOnHomeRank: 1,
|
<td>
|
||||||
})
|
<ShortToggle
|
||||||
.then(() => {
|
enabled={contract.visibility === 'unlisted'}
|
||||||
setFeatured(true)
|
setEnabled={(b) =>
|
||||||
})
|
updateContract(id, {
|
||||||
.catch(console.error)
|
visibility: b ? 'unlisted' : 'public',
|
||||||
else if (
|
})
|
||||||
!newVal &&
|
}
|
||||||
(contract?.featuredOnHomeRank ?? 0) > 0
|
/>
|
||||||
)
|
|
||||||
updateContract(id, {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
featuredOnHomeRank: deleteField(),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setFeatured(false)
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="false">false</option>
|
|
||||||
<option value="true">true</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -75,36 +75,30 @@ export const ContractOverview = (props: {
|
||||||
{isBinary ? (
|
{isBinary ? (
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
<Row className={'items-center justify-center'}>
|
{tradingAllowed(contract) && (
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
<Col>
|
||||||
{tradingAllowed(contract) && (
|
<BetButton contract={contract as CPMMBinaryContract} />
|
||||||
<Col>
|
{!user && (
|
||||||
<BetButton contract={contract as CPMMBinaryContract} />
|
<div className="mt-1 text-center text-sm text-gray-500">
|
||||||
{!user && (
|
(with play money!)
|
||||||
<div className="mt-1 text-center text-sm text-gray-500">
|
</div>
|
||||||
(with play money!)
|
)}
|
||||||
</div>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Row>
|
</Row>
|
||||||
) : isPseudoNumeric ? (
|
) : isPseudoNumeric ? (
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
<Row className={'items-center justify-center'}>
|
{tradingAllowed(contract) && (
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
<Col>
|
||||||
{tradingAllowed(contract) && (
|
<BetButton contract={contract} />
|
||||||
<Col>
|
{!user && (
|
||||||
<BetButton contract={contract} />
|
<div className="mt-1 text-center text-sm text-gray-500">
|
||||||
{!user && (
|
(with play money!)
|
||||||
<div className="mt-1 text-center text-sm text-gray-500">
|
</div>
|
||||||
(with play money!)
|
)}
|
||||||
</div>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
(outcomeType === 'FREE_RESPONSE' ||
|
(outcomeType === 'FREE_RESPONSE' ||
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
}) {
|
}) {
|
||||||
const { contract, height } = props
|
const { contract, height } = props
|
||||||
const { resolutionTime, closeTime, outcomeType } = contract
|
const { resolutionTime, closeTime, outcomeType } = contract
|
||||||
|
const now = Date.now()
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const isLogScale = outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
const isLogScale = outcomeType === 'PSEUDO_NUMERIC' && contract.isLogScale
|
||||||
|
|
||||||
|
@ -23,10 +24,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
|
|
||||||
const startProb = getInitialProbability(contract)
|
const startProb = getInitialProbability(contract)
|
||||||
|
|
||||||
const times = [
|
const times = [contract.createdTime, ...bets.map((bet) => bet.createdTime)]
|
||||||
contract.createdTime,
|
|
||||||
...bets.map((bet) => bet.createdTime),
|
|
||||||
].map((time) => new Date(time))
|
|
||||||
|
|
||||||
const f: (p: number) => number = isBinary
|
const f: (p: number) => number = isBinary
|
||||||
? (p) => p
|
? (p) => p
|
||||||
|
@ -36,17 +34,17 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
|
|
||||||
const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f)
|
const probs = [startProb, ...bets.map((bet) => bet.probAfter)].map(f)
|
||||||
|
|
||||||
const isClosed = !!closeTime && Date.now() > closeTime
|
const isClosed = !!closeTime && now > closeTime
|
||||||
const latestTime = dayjs(
|
const latestTime = dayjs(
|
||||||
resolutionTime && isClosed
|
resolutionTime && isClosed
|
||||||
? Math.min(resolutionTime, closeTime)
|
? Math.min(resolutionTime, closeTime)
|
||||||
: isClosed
|
: isClosed
|
||||||
? closeTime
|
? closeTime
|
||||||
: resolutionTime ?? Date.now()
|
: resolutionTime ?? now
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add a fake datapoint so the line continues to the right
|
// Add a fake datapoint so the line continues to the right
|
||||||
times.push(latestTime.toDate())
|
times.push(latestTime.valueOf())
|
||||||
probs.push(probs[probs.length - 1])
|
probs.push(probs[probs.length - 1])
|
||||||
|
|
||||||
const quartiles = [0, 25, 50, 75, 100]
|
const quartiles = [0, 25, 50, 75, 100]
|
||||||
|
@ -58,15 +56,16 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||||
const hoursAgo = latestTime.subtract(1, 'hours')
|
const startDate = dayjs(times[0])
|
||||||
const startDate = dayjs(times[0]).isBefore(hoursAgo)
|
const endDate = startDate.add(1, 'hour').isAfter(latestTime)
|
||||||
? times[0]
|
? latestTime.add(1, 'hours')
|
||||||
: hoursAgo.toDate()
|
: latestTime
|
||||||
|
const includeMinute = endDate.diff(startDate, 'hours') < 2
|
||||||
|
|
||||||
// Minimum number of points for the graph to have. For smooth tooltip movement
|
// Minimum number of points for the graph to have. For smooth tooltip movement
|
||||||
// On first load, width is undefined, skip adding extra points to let page load faster
|
// If we aren't actually loading any data yet, skip adding extra points to let page load faster
|
||||||
// This fn runs again once DOM is finished loading
|
// This fn runs again once DOM is finished loading
|
||||||
const totalPoints = width ? (width > 800 ? 300 : 50) : 1
|
const totalPoints = width && bets.length ? (width > 800 ? 300 : 50) : 1
|
||||||
|
|
||||||
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
|
const timeStep: number = latestTime.diff(startDate, 'ms') / totalPoints
|
||||||
|
|
||||||
|
@ -74,20 +73,16 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
const s = isBinary ? 100 : 1
|
const s = isBinary ? 100 : 1
|
||||||
|
|
||||||
for (let i = 0; i < times.length - 1; i++) {
|
for (let i = 0; i < times.length - 1; i++) {
|
||||||
points[points.length] = { x: times[i], y: s * probs[i] }
|
const p = probs[i]
|
||||||
const numPoints: number = Math.floor(
|
const d0 = times[i]
|
||||||
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / timeStep
|
const d1 = times[i + 1]
|
||||||
)
|
const msDiff = d1 - d0
|
||||||
|
const numPoints = Math.floor(msDiff / timeStep)
|
||||||
|
points.push({ x: new Date(times[i]), y: s * p })
|
||||||
if (numPoints > 1) {
|
if (numPoints > 1) {
|
||||||
const thisTimeStep: number =
|
const thisTimeStep: number = msDiff / numPoints
|
||||||
dayjs(times[i + 1]).diff(dayjs(times[i]), 'ms') / numPoints
|
|
||||||
for (let n = 1; n < numPoints; n++) {
|
for (let n = 1; n < numPoints; n++) {
|
||||||
points[points.length] = {
|
points.push({ x: new Date(d0 + thisTimeStep * n), y: s * p })
|
||||||
x: dayjs(times[i])
|
|
||||||
.add(thisTimeStep * n, 'ms')
|
|
||||||
.toDate(),
|
|
||||||
y: s * probs[i],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,8 +91,8 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
{ id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' },
|
{ id: 'Yes', data: points, color: isBinary ? '#11b981' : '#5fa5f9' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const multiYear = !dayjs(startDate).isSame(latestTime, 'year')
|
const multiYear = !startDate.isSame(latestTime, 'year')
|
||||||
const lessThanAWeek = dayjs(startDate).add(8, 'day').isAfter(latestTime)
|
const lessThanAWeek = startDate.add(8, 'day').isAfter(latestTime)
|
||||||
|
|
||||||
const formatter = isBinary
|
const formatter = isBinary
|
||||||
? formatPercent
|
? formatPercent
|
||||||
|
@ -132,15 +127,16 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
|
||||||
}}
|
}}
|
||||||
xScale={{
|
xScale={{
|
||||||
type: 'time',
|
type: 'time',
|
||||||
min: startDate,
|
min: startDate.toDate(),
|
||||||
max: latestTime.toDate(),
|
max: endDate.toDate(),
|
||||||
}}
|
}}
|
||||||
xFormat={(d) =>
|
xFormat={(d) =>
|
||||||
formatTime(+d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
formatTime(now, +d.valueOf(), multiYear, lessThanAWeek, lessThanAWeek)
|
||||||
}
|
}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
tickValues: numXTickValues,
|
tickValues: numXTickValues,
|
||||||
format: (time) => formatTime(+time, multiYear, lessThanAWeek, false),
|
format: (time) =>
|
||||||
|
formatTime(now, +time, multiYear, lessThanAWeek, includeMinute),
|
||||||
}}
|
}}
|
||||||
colors={{ datum: 'color' }}
|
colors={{ datum: 'color' }}
|
||||||
curve="stepAfter"
|
curve="stepAfter"
|
||||||
|
@ -176,19 +172,20 @@ function formatPercent(y: DatumValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(
|
function formatTime(
|
||||||
|
now: number,
|
||||||
time: number,
|
time: number,
|
||||||
includeYear: boolean,
|
includeYear: boolean,
|
||||||
includeHour: boolean,
|
includeHour: boolean,
|
||||||
includeMinute: boolean
|
includeMinute: boolean
|
||||||
) {
|
) {
|
||||||
const d = dayjs(time)
|
const d = dayjs(time)
|
||||||
|
if (d.add(1, 'minute').isAfter(now) && d.subtract(1, 'minute').isBefore(now))
|
||||||
if (d.add(1, 'minute').isAfter(Date.now())) return 'Now'
|
return 'Now'
|
||||||
|
|
||||||
let format: string
|
let format: string
|
||||||
if (d.isSame(Date.now(), 'day')) {
|
if (d.isSame(now, 'day')) {
|
||||||
format = '[Today]'
|
format = '[Today]'
|
||||||
} else if (d.add(1, 'day').isSame(Date.now(), 'day')) {
|
} else if (d.add(1, 'day').isSame(now, 'day')) {
|
||||||
format = '[Yesterday]'
|
format = '[Yesterday]'
|
||||||
} else {
|
} else {
|
||||||
format = 'MMM D'
|
format = 'MMM D'
|
||||||
|
|
|
@ -26,6 +26,7 @@ export function ContractsGrid(props: {
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
}
|
}
|
||||||
highlightOptions?: ContractHighlightOptions
|
highlightOptions?: ContractHighlightOptions
|
||||||
|
trackingPostfix?: string
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
contracts,
|
contracts,
|
||||||
|
@ -34,6 +35,7 @@ export function ContractsGrid(props: {
|
||||||
onContractClick,
|
onContractClick,
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
|
trackingPostfix,
|
||||||
} = props
|
} = props
|
||||||
const { hideQuickBet, hideGroupLink } = cardHideOptions || {}
|
const { hideQuickBet, hideGroupLink } = cardHideOptions || {}
|
||||||
const { contractIds, highlightClassName } = highlightOptions || {}
|
const { contractIds, highlightClassName } = highlightOptions || {}
|
||||||
|
@ -79,6 +81,7 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
hideQuickBet={hideQuickBet}
|
hideQuickBet={hideQuickBet}
|
||||||
hideGroupLink={hideGroupLink}
|
hideGroupLink={hideGroupLink}
|
||||||
|
trackingPostfix={trackingPostfix}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mb-4 break-inside-avoid-column overflow-hidden', // prevent content from wrapping (needs overflow on firefox)
|
'mb-4 break-inside-avoid-column overflow-hidden', // prevent content from wrapping (needs overflow on firefox)
|
||||||
contractIds?.includes(contract.id) && highlightClassName
|
contractIds?.includes(contract.id) && highlightClassName
|
||||||
|
|
9
web/components/contract/featured-contract-badge.tsx
Normal file
9
web/components/contract/featured-contract-badge.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { BadgeCheckIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
export function FeaturedContractBadge() {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-3 py-0.5 text-sm font-medium text-green-800">
|
||||||
|
<BadgeCheckIcon className="h-4 w-4" aria-hidden="true" /> Featured
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
|
@ -41,8 +41,9 @@ const BET_SIZE = 10
|
||||||
export function QuickBet(props: {
|
export function QuickBet(props: {
|
||||||
contract: BinaryContract | PseudoNumericContract
|
contract: BinaryContract | PseudoNumericContract
|
||||||
user: User
|
user: User
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, user } = props
|
const { contract, user, className } = props
|
||||||
const { mechanism, outcomeType } = contract
|
const { mechanism, outcomeType } = contract
|
||||||
const isCpmm = mechanism === 'cpmm-1'
|
const isCpmm = mechanism === 'cpmm-1'
|
||||||
|
|
||||||
|
@ -139,6 +140,7 @@ export function QuickBet(props: {
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
className,
|
||||||
'relative min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle'
|
'relative min-w-[5.5rem] justify-center gap-2 pr-5 pl-1 align-middle'
|
||||||
// Use this for colored QuickBet panes
|
// Use this for colored QuickBet panes
|
||||||
// `bg-opacity-10 bg-${color}`
|
// `bg-opacity-10 bg-${color}`
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { firebaseLogin, User } from 'web/lib/firebase/users'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export const createButtonStyle =
|
import { User } from 'web/lib/firebase/users'
|
||||||
'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11'
|
import { Button } from './button'
|
||||||
|
|
||||||
export const CreateQuestionButton = (props: {
|
export const CreateQuestionButton = (props: {
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
|
@ -13,32 +11,17 @@ export const CreateQuestionButton = (props: {
|
||||||
className?: string
|
className?: string
|
||||||
query?: string
|
query?: string
|
||||||
}) => {
|
}) => {
|
||||||
const gradient =
|
|
||||||
'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700'
|
|
||||||
|
|
||||||
const { user, overrideText, className, query } = props
|
const { user, overrideText, className, query } = props
|
||||||
const router = useRouter()
|
|
||||||
|
if (!user || user?.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex justify-center', className)}>
|
<div className={clsx('flex justify-center', className)}>
|
||||||
{user ? (
|
<Link href={`/create${query ? query : ''}`} passHref>
|
||||||
<Link href={`/create${query ? query : ''}`} passHref>
|
<Button color="gradient" size="xl" className="mt-4">
|
||||||
<button className={clsx(gradient, createButtonStyle)}>
|
{overrideText ?? 'Create a market'}
|
||||||
{overrideText ? overrideText : 'Create a market'}
|
</Button>
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
// login, and then reload the page, to hit any SSR redirect (e.g.
|
|
||||||
// redirecting from / to /home for logged in users)
|
|
||||||
await firebaseLogin()
|
|
||||||
router.replace(router.asPath)
|
|
||||||
}}
|
|
||||||
className={clsx(gradient, createButtonStyle)}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import dayjs, { Dayjs } from 'dayjs'
|
|
||||||
import utc from 'dayjs/plugin/utc'
|
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
|
||||||
import advanced from 'dayjs/plugin/advancedFormat'
|
|
||||||
import { Tooltip } from './tooltip'
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
const FORMATTER = new Intl.DateTimeFormat('default', {
|
||||||
dayjs.extend(timezone)
|
dateStyle: 'medium',
|
||||||
dayjs.extend(advanced)
|
timeStyle: 'long',
|
||||||
|
})
|
||||||
|
|
||||||
export function DateTimeTooltip(props: {
|
export function DateTimeTooltip(props: {
|
||||||
time: Dayjs
|
time: number
|
||||||
text?: string
|
text?: string
|
||||||
className?: string
|
className?: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
@ -17,7 +14,7 @@ export function DateTimeTooltip(props: {
|
||||||
}) {
|
}) {
|
||||||
const { className, time, text, noTap } = props
|
const { className, time, text, noTap } = props
|
||||||
|
|
||||||
const formattedTime = time.format('MMM DD, YYYY hh:mm a z')
|
const formattedTime = FORMATTER.format(time)
|
||||||
const toolTip = text ? `${text} ${formattedTime}` : formattedTime
|
const toolTip = text ? `${text} ${formattedTime}` : formattedTime
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -40,6 +40,11 @@ const embedPatterns: EmbedPattern[] = [
|
||||||
rewrite: (id) =>
|
rewrite: (id) =>
|
||||||
`<iframe src="https://www.metaculus.com/questions/embed/${id}"></iframe>`,
|
`<iframe src="https://www.metaculus.com/questions/embed/${id}"></iframe>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
regex: /^(https?:\/\/www\.figma\.com\/(?:file|proto)\/[^\/]+\/[^\/]+)/,
|
||||||
|
rewrite: (url) =>
|
||||||
|
`<iframe src="https://www.figma.com/embed?embed_host=manifold&url=${url}"></iframe>`,
|
||||||
|
},
|
||||||
// Twitch is a bit annoying, since it requires the `&parent=DOMAIN` to match
|
// Twitch is a bit annoying, since it requires the `&parent=DOMAIN` to match
|
||||||
{
|
{
|
||||||
// Twitch: https://www.twitch.tv/videos/1445087149
|
// Twitch: https://www.twitch.tv/videos/1445087149
|
||||||
|
|
|
@ -48,8 +48,17 @@ export function MarketModal(props: {
|
||||||
{contracts.length > 1 && 's'}
|
{contracts.length > 1 && 's'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => setContracts([])} color="gray">
|
<Button
|
||||||
Cancel
|
onClick={() => {
|
||||||
|
if (contracts.length > 0) {
|
||||||
|
setContracts([])
|
||||||
|
} else {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="gray"
|
||||||
|
>
|
||||||
|
{contracts.length > 0 ? 'Reset' : 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,8 +6,6 @@ import Link from 'next/link'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
import { LinkIcon } from '@heroicons/react/outline'
|
import { LinkIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
export function CopyLinkDateTimeComponent(props: {
|
export function CopyLinkDateTimeComponent(props: {
|
||||||
prefix: string
|
prefix: string
|
||||||
|
@ -18,7 +16,6 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
}) {
|
}) {
|
||||||
const { prefix, slug, elementId, createdTime, className } = props
|
const { prefix, slug, elementId, createdTime, className } = props
|
||||||
const [showToast, setShowToast] = useState(false)
|
const [showToast, setShowToast] = useState(false)
|
||||||
const time = dayjs(createdTime)
|
|
||||||
|
|
||||||
function copyLinkToComment(
|
function copyLinkToComment(
|
||||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
|
@ -31,26 +28,19 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={clsx('inline', className)}>
|
<DateTimeTooltip className={className} time={createdTime} noTap>
|
||||||
<DateTimeTooltip time={time} noTap>
|
<Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}>
|
||||||
<Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}>
|
<a
|
||||||
<a
|
onClick={copyLinkToComment}
|
||||||
onClick={(event) => copyLinkToComment(event)}
|
className={
|
||||||
className={'mx-1 cursor-pointer'}
|
'mx-1 whitespace-nowrap rounded-sm px-1 text-gray-400 hover:bg-gray-100'
|
||||||
>
|
}
|
||||||
<span className="whitespace-nowrap rounded-sm px-1 text-gray-400 hover:bg-gray-100 ">
|
>
|
||||||
{fromNow(createdTime)}
|
{fromNow(createdTime)}
|
||||||
{showToast && (
|
{showToast && <ToastClipboard />}
|
||||||
<ToastClipboard className={'left-24 sm:-left-16'} />
|
<LinkIcon className="ml-1 mb-0.5 inline" height={13} />
|
||||||
)}
|
</a>
|
||||||
<LinkIcon
|
</Link>
|
||||||
className="ml-1 mb-0.5 inline-block text-gray-400"
|
</DateTimeTooltip>
|
||||||
height={13}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</DateTimeTooltip>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { useUser, useUserById } from 'web/hooks/use-user'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { UsersIcon } from '@heroicons/react/solid'
|
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { OutcomeLabel } from 'web/components/outcome-label'
|
import { OutcomeLabel } from 'web/components/outcome-label'
|
||||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||||
|
@ -154,79 +153,3 @@ export function BetStatusText(props: {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetGroupSpan(props: {
|
|
||||||
contract: Contract
|
|
||||||
bets: Bet[]
|
|
||||||
outcome?: string
|
|
||||||
}) {
|
|
||||||
const { contract, bets, outcome } = props
|
|
||||||
|
|
||||||
const numberTraders = uniqBy(bets, (b) => b.userId).length
|
|
||||||
|
|
||||||
const [buys, sells] = partition(bets, (bet) => bet.amount >= 0)
|
|
||||||
const buyTotal = sumBy(buys, (b) => b.amount)
|
|
||||||
const sellTotal = sumBy(sells, (b) => -b.amount)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{numberTraders} {numberTraders > 1 ? 'traders' : 'trader'}{' '}
|
|
||||||
<JoinSpans>
|
|
||||||
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
|
||||||
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
|
||||||
</JoinSpans>
|
|
||||||
{outcome && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
of{' '}
|
|
||||||
<OutcomeLabel
|
|
||||||
outcome={outcome}
|
|
||||||
contract={contract}
|
|
||||||
truncate="short"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}{' '}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeedBetGroup(props: {
|
|
||||||
contract: Contract
|
|
||||||
bets: Bet[]
|
|
||||||
hideOutcome: boolean
|
|
||||||
}) {
|
|
||||||
const { contract, bets, hideOutcome } = props
|
|
||||||
|
|
||||||
const betGroups = groupBy(bets, (bet) => bet.outcome)
|
|
||||||
const outcomes = Object.keys(betGroups)
|
|
||||||
|
|
||||||
// Use the time of the last bet for the entire group
|
|
||||||
const createdTime = bets[bets.length - 1].createdTime
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<div className="relative px-1">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
|
||||||
<UsersIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={clsx('min-w-0 flex-1', outcomes.length === 1 && 'mt-1')}>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{outcomes.map((outcome, index) => (
|
|
||||||
<Fragment key={outcome}>
|
|
||||||
<BetGroupSpan
|
|
||||||
contract={contract}
|
|
||||||
outcome={hideOutcome ? undefined : outcome}
|
|
||||||
bets={betGroups[outcome]}
|
|
||||||
/>
|
|
||||||
{index !== outcomes.length - 1 && <br />}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
<RelativeTimestamp time={createdTime} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -382,6 +382,8 @@ export function CommentInput(props: {
|
||||||
|
|
||||||
const isNumeric = contract.outcomeType === 'NUMERIC'
|
const isNumeric = contract.outcomeType === 'NUMERIC'
|
||||||
|
|
||||||
|
if (user?.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className={'mb-2 gap-1 sm:gap-2'}>
|
<Row className={'mb-2 gap-1 sm:gap-2'}>
|
||||||
|
@ -535,7 +537,7 @@ export function CommentInputTextArea(props: {
|
||||||
className={'btn btn-outline btn-sm mt-2 normal-case'}
|
className={'btn btn-outline btn-sm mt-2 normal-case'}
|
||||||
onClick={() => submitComment(presetId)}
|
onClick={() => submitComment(presetId)}
|
||||||
>
|
>
|
||||||
Sign in to comment
|
Add my comment
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
import { CPMMBinaryContract, NumericContract } from 'common/contract'
|
import { CPMMBinaryContract, NumericContract } from 'common/contract'
|
||||||
import { FeedLiquidity } from './feed-liquidity'
|
import { FeedLiquidity } from './feed-liquidity'
|
||||||
import { SignUpPrompt } from '../sign-up-prompt'
|
import { BetSignUpPrompt } from '../sign-up-prompt'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
||||||
import { contractMetrics } from 'common/contract-details'
|
import { contractMetrics } from 'common/contract-details'
|
||||||
|
@ -70,7 +70,7 @@ export function FeedItems(props: {
|
||||||
|
|
||||||
{!user ? (
|
{!user ? (
|
||||||
<Col className="mt-4 max-w-sm items-center xl:hidden">
|
<Col className="mt-4 max-w-sm items-center xl:hidden">
|
||||||
<SignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
<PlayMoneyDisclaimer />
|
<PlayMoneyDisclaimer />
|
||||||
</Col>
|
</Col>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -10,7 +10,11 @@ export function FileUploadButton(props: {
|
||||||
const ref = useRef<HTMLInputElement>(null)
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button className={className} onClick={() => ref.current?.click()}>
|
<button
|
||||||
|
type={'button'}
|
||||||
|
className={className}
|
||||||
|
onClick={() => ref.current?.click()}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -76,6 +76,8 @@ export function CreateGroupButton(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { sum } from 'lodash'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import { Content, useTextEditor } from 'web/components/editor'
|
import { Content, useTextEditor } from 'web/components/editor'
|
||||||
import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
|
import { useUnseenNotifications } from 'web/hooks/use-notifications'
|
||||||
import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
|
import { ChevronDownIcon, UsersIcon } from '@heroicons/react/outline'
|
||||||
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
||||||
import { usePrivateUser } from 'web/hooks/use-user'
|
import { usePrivateUser } from 'web/hooks/use-user'
|
||||||
|
@ -277,14 +277,18 @@ function GroupChatNotificationsIcon(props: {
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
}) {
|
}) {
|
||||||
const { privateUser, group, shouldSetAsSeen, hidden } = props
|
const { privateUser, group, shouldSetAsSeen, hidden } = props
|
||||||
const preferredNotificationsForThisGroup = useUnseenPreferredNotifications(
|
const notificationsForThisGroup = useUnseenNotifications(
|
||||||
privateUser,
|
privateUser
|
||||||
{
|
// Disabled tracking by customHref for now.
|
||||||
customHref: `/group/${group.slug}`,
|
// {
|
||||||
}
|
// customHref: `/group/${group.slug}`,
|
||||||
|
// }
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preferredNotificationsForThisGroup.forEach((notification) => {
|
if (!notificationsForThisGroup) return
|
||||||
|
|
||||||
|
notificationsForThisGroup.forEach((notification) => {
|
||||||
if (
|
if (
|
||||||
(shouldSetAsSeen && notification.isSeenOnHref?.includes('chat')) ||
|
(shouldSetAsSeen && notification.isSeenOnHref?.includes('chat')) ||
|
||||||
// old style chat notif that simply ended with the group slug
|
// old style chat notif that simply ended with the group slug
|
||||||
|
@ -293,13 +297,14 @@ function GroupChatNotificationsIcon(props: {
|
||||||
setNotificationsAsSeen([notification])
|
setNotificationsAsSeen([notification])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [group.slug, preferredNotificationsForThisGroup, shouldSetAsSeen])
|
}, [group.slug, notificationsForThisGroup, shouldSetAsSeen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
!hidden &&
|
!hidden &&
|
||||||
preferredNotificationsForThisGroup.length > 0 &&
|
notificationsForThisGroup &&
|
||||||
|
notificationsForThisGroup.length > 0 &&
|
||||||
!shouldSetAsSeen
|
!shouldSetAsSeen
|
||||||
? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500'
|
? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500'
|
||||||
: 'hidden'
|
: 'hidden'
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { Tooltip } from './tooltip'
|
||||||
export function InfoTooltip(props: { text: string }) {
|
export function InfoTooltip(props: { text: string }) {
|
||||||
const { text } = props
|
const { text } = props
|
||||||
return (
|
return (
|
||||||
<Tooltip text={text}>
|
<Tooltip className="inline-block" text={text}>
|
||||||
<InformationCircleIcon className="h-5 w-5 text-gray-500" />
|
<InformationCircleIcon className="-mb-1 h-5 w-5 text-gray-500" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function Leaderboard(props: {
|
||||||
{users.map((user, index) => (
|
{users.map((user, index) => (
|
||||||
<tr key={user.id}>
|
<tr key={user.id}>
|
||||||
<td>{index + 1}</td>
|
<td>{index + 1}</td>
|
||||||
<td style={{ maxWidth: 190 }}>
|
<td className="max-w-[190px]">
|
||||||
<SiteLink className="relative" href={`/${user.username}`}>
|
<SiteLink className="relative" href={`/${user.username}`}>
|
||||||
<Row className="items-center gap-4">
|
<Row className="items-center gap-4">
|
||||||
<Avatar avatarUrl={user.avatarUrl} size={8} />
|
<Avatar avatarUrl={user.avatarUrl} size={8} />
|
||||||
|
|
|
@ -38,10 +38,7 @@ export function Linkify(props: { text: string; gray?: boolean }) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<span
|
<span className="break-anywhere">
|
||||||
className="break-words"
|
|
||||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
|
||||||
>
|
|
||||||
{text.split(regex).map((part, i) => (
|
{text.split(regex).map((part, i) => (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
{part}
|
{part}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import {
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
CashIcon,
|
CashIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
UserGroupIcon,
|
|
||||||
TrendingUpIcon,
|
|
||||||
ChatIcon,
|
ChatIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
@ -18,17 +16,14 @@ import { ManifoldLogo } from './manifold-logo'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
import { ProfileSummary } from './profile-menu'
|
import { ProfileSummary } from './profile-menu'
|
||||||
import NotificationsIcon from 'web/components/notifications-icon'
|
import NotificationsIcon from 'web/components/notifications-icon'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { CreateQuestionButton } from 'web/components/create-question-button'
|
import { CreateQuestionButton } from 'web/components/create-question-button'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
|
||||||
import { trackCallback, withTracking } from 'web/lib/service/analytics'
|
import { trackCallback, withTracking } from 'web/lib/service/analytics'
|
||||||
import { Group } from 'common/group'
|
|
||||||
import { Spacer } from '../layout/spacer'
|
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
import { buildArray } from 'common/util/array'
|
import { buildArray } from 'common/util/array'
|
||||||
|
import TrophyIcon from 'web/lib/icons/trophy-icon'
|
||||||
|
import { SignInButton } from '../sign-in-button'
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
// log out, and then reload the page, in case SSR wants to boot them out
|
// log out, and then reload the page, in case SSR wants to boot them out
|
||||||
|
@ -46,11 +41,12 @@ function getNavigation() {
|
||||||
icon: NotificationsIcon,
|
icon: NotificationsIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
|
||||||
|
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? []
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
: [
|
||||||
|
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||||
|
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,14 +63,14 @@ function getMoreNavigation(user?: User | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
// Signed out "More"
|
||||||
return buildArray(
|
return buildArray(
|
||||||
|
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||||
|
{ name: 'Groups', href: '/groups' },
|
||||||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||||
[
|
[
|
||||||
|
{ name: 'Tournaments', href: '/tournaments' },
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
{
|
|
||||||
name: 'Salem tournament',
|
|
||||||
href: 'https://salemcenter.manifold.markets/',
|
|
||||||
},
|
|
||||||
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||||
|
@ -82,16 +78,15 @@ function getMoreNavigation(user?: User | null) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signed in "More"
|
||||||
return buildArray(
|
return buildArray(
|
||||||
|
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||||
|
{ name: 'Groups', href: '/groups' },
|
||||||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||||
[
|
[
|
||||||
{ name: 'Referrals', href: '/referrals' },
|
{ name: 'Referrals', href: '/referrals' },
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
{ name: 'Send M$', href: '/links' },
|
{ name: 'Send M$', href: '/links' },
|
||||||
{
|
|
||||||
name: 'Salem tournament',
|
|
||||||
href: 'https://salemcenter.manifold.markets/',
|
|
||||||
},
|
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
{ name: 'Help & About', href: 'https://help.manifold.markets/' },
|
{ name: 'Help & About', href: 'https://help.manifold.markets/' },
|
||||||
{
|
{
|
||||||
|
@ -120,12 +115,12 @@ const signedOutMobileNavigation = [
|
||||||
icon: BookOpenIcon,
|
icon: BookOpenIcon,
|
||||||
},
|
},
|
||||||
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon },
|
||||||
]
|
]
|
||||||
|
|
||||||
const signedInMobileNavigation = [
|
const signedInMobileNavigation = [
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
{ name: 'Tournaments', href: '/tournaments', icon: TrophyIcon },
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? []
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||||
|
@ -147,11 +142,9 @@ function getMoreMobileNav() {
|
||||||
return buildArray<Item>(
|
return buildArray<Item>(
|
||||||
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
|
||||||
[
|
[
|
||||||
|
{ name: 'Groups', href: '/groups' },
|
||||||
{ name: 'Referrals', href: '/referrals' },
|
{ name: 'Referrals', href: '/referrals' },
|
||||||
{
|
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||||
name: 'Salem tournament',
|
|
||||||
href: 'https://salemcenter.manifold.markets/',
|
|
||||||
},
|
|
||||||
{ name: 'Charity', href: '/charity' },
|
{ name: 'Charity', href: '/charity' },
|
||||||
{ name: 'Send M$', href: '/links' },
|
{ name: 'Send M$', href: '/links' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
|
@ -232,29 +225,23 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
? signedOutMobileNavigation
|
? signedOutMobileNavigation
|
||||||
: signedInMobileNavigation
|
: signedInMobileNavigation
|
||||||
|
|
||||||
const memberItems = (
|
|
||||||
useMemberGroups(user?.id, undefined, {
|
|
||||||
by: 'mostRecentContractAddedTime',
|
|
||||||
}) ?? []
|
|
||||||
).map((group: Group) => ({
|
|
||||||
name: group.name,
|
|
||||||
href: `${groupPath(group.slug)}`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="Sidebar" className={className}>
|
<nav
|
||||||
|
aria-label="Sidebar"
|
||||||
|
className={clsx('flex max-h-[100vh] flex-col', className)}
|
||||||
|
>
|
||||||
<ManifoldLogo className="py-6" twoLine />
|
<ManifoldLogo className="py-6" twoLine />
|
||||||
|
|
||||||
<CreateQuestionButton user={user} />
|
{!user && <SignInButton className="mb-4" />}
|
||||||
<Spacer h={4} />
|
|
||||||
{user && (
|
{user && (
|
||||||
<div className="w-full" style={{ minHeight: 80 }}>
|
<div className="min-h-[80px] w-full">
|
||||||
<ProfileSummary user={user} />
|
<ProfileSummary user={user} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile navigation */}
|
{/* Mobile navigation */}
|
||||||
<div className="space-y-1 lg:hidden">
|
<div className="flex min-h-0 shrink flex-col gap-1 lg:hidden">
|
||||||
{mobileNavigationOptions.map((item) => (
|
{mobileNavigationOptions.map((item) => (
|
||||||
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
||||||
))}
|
))}
|
||||||
|
@ -265,15 +252,10 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
buttonContent={<MoreButton />}
|
buttonContent={<MoreButton />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Spacer if there are any groups */}
|
|
||||||
{memberItems.length > 0 && (
|
|
||||||
<hr className="!my-4 mr-2 border-gray-300" />
|
|
||||||
)}
|
|
||||||
<GroupsList currentPage={router.asPath} memberItems={memberItems} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop navigation */}
|
{/* Desktop navigation */}
|
||||||
<div className="hidden space-y-1 lg:block">
|
<div className="hidden min-h-0 shrink flex-col gap-1 lg:flex">
|
||||||
{navigationOptions.map((item) => (
|
{navigationOptions.map((item) => (
|
||||||
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
||||||
))}
|
))}
|
||||||
|
@ -282,65 +264,8 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
buttonContent={<MoreButton />}
|
buttonContent={<MoreButton />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Spacer if there are any groups */}
|
{user && <CreateQuestionButton user={user} />}
|
||||||
{memberItems.length > 0 && <hr className="!my-4 border-gray-300" />}
|
|
||||||
<GroupsList currentPage={router.asPath} memberItems={memberItems} />
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
|
||||||
const { currentPage, memberItems } = props
|
|
||||||
|
|
||||||
const { height } = useWindowSize()
|
|
||||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null)
|
|
||||||
const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0)
|
|
||||||
|
|
||||||
// const preferredNotifications = useUnseenPreferredNotifications(
|
|
||||||
// privateUser,
|
|
||||||
// {
|
|
||||||
// customHref: '/group/',
|
|
||||||
// },
|
|
||||||
// memberItems.length > 0 ? memberItems.length : undefined
|
|
||||||
// )
|
|
||||||
// const notifIsForThisItem = useMemo(
|
|
||||||
// () => (itemHref: string) =>
|
|
||||||
// preferredNotifications.some(
|
|
||||||
// (n) =>
|
|
||||||
// !n.isSeen &&
|
|
||||||
// (n.isSeenOnHref === itemHref ||
|
|
||||||
// n.isSeenOnHref?.replace('/chat', '') === itemHref)
|
|
||||||
// ),
|
|
||||||
// [preferredNotifications]
|
|
||||||
// )
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SidebarItem
|
|
||||||
item={{ name: 'Groups', href: '/groups', icon: UserGroupIcon }}
|
|
||||||
currentPage={currentPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex-1 space-y-0.5 overflow-auto"
|
|
||||||
style={{ height: remainingHeight }}
|
|
||||||
ref={setContainerRef}
|
|
||||||
>
|
|
||||||
{memberItems.map((item) => (
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
key={item.name}
|
|
||||||
onClick={trackCallback('click sidebar group', { name: item.name })}
|
|
||||||
className={clsx(
|
|
||||||
'cursor-pointer truncate',
|
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Row } from 'web/components/layout/row'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { usePrivateUser } from 'web/hooks/use-user'
|
import { usePrivateUser } from 'web/hooks/use-user'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications'
|
import { useUnseenGroupedNotification } from 'web/hooks/use-notifications'
|
||||||
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
||||||
import { PrivateUser } from 'common/user'
|
import { PrivateUser } from 'common/user'
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ function UnseenNotificationsBubble(props: { privateUser: PrivateUser }) {
|
||||||
else setSeen(false)
|
else setSeen(false)
|
||||||
}, [router.pathname])
|
}, [router.pathname])
|
||||||
|
|
||||||
const notifications = useUnseenPreferredNotificationGroups(privateUser)
|
const notifications = useUnseenGroupedNotification(privateUser)
|
||||||
if (!notifications || notifications.length === 0 || seen) {
|
if (!notifications || notifications.length === 0 || seen) {
|
||||||
return <div />
|
return <div />
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { BucketInput } from './bucket-input'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { SignUpPrompt } from './sign-up-prompt'
|
import { BetSignUpPrompt } from './sign-up-prompt'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
export function NumericBetPanel(props: {
|
export function NumericBetPanel(props: {
|
||||||
|
@ -34,7 +34,7 @@ export function NumericBetPanel(props: {
|
||||||
|
|
||||||
<NumericBuyPanel contract={contract} user={user} />
|
<NumericBuyPanel contract={contract} user={user} />
|
||||||
|
|
||||||
<SignUpPrompt />
|
<BetSignUpPrompt />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,10 +164,7 @@ export function AnswerLabel(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip text={truncated === text ? false : text}>
|
<Tooltip text={truncated === text ? false : text}>
|
||||||
<span
|
<span className={clsx('break-anywhere whitespace-pre-line', className)}>
|
||||||
style={{ wordBreak: 'break-word' }}
|
|
||||||
className={clsx('whitespace-pre-line break-words', className)}
|
|
||||||
>
|
|
||||||
{truncated}
|
{truncated}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -6,13 +6,11 @@ import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
export function Page(props: {
|
export function Page(props: {
|
||||||
rightSidebar?: ReactNode
|
rightSidebar?: ReactNode
|
||||||
suspend?: boolean
|
|
||||||
className?: string
|
className?: string
|
||||||
rightSidebarClassName?: string
|
rightSidebarClassName?: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { children, rightSidebar, suspend, className, rightSidebarClassName } =
|
const { children, rightSidebar, className, rightSidebarClassName } = props
|
||||||
props
|
|
||||||
|
|
||||||
const bottomBarPadding = 'pb-[58px] lg:pb-0 '
|
const bottomBarPadding = 'pb-[58px] lg:pb-0 '
|
||||||
return (
|
return (
|
||||||
|
@ -23,10 +21,9 @@ export function Page(props: {
|
||||||
bottomBarPadding,
|
bottomBarPadding,
|
||||||
'mx-auto w-full lg:grid lg:grid-cols-12 lg:gap-x-2 xl:max-w-7xl xl:gap-x-8'
|
'mx-auto w-full lg:grid lg:grid-cols-12 lg:gap-x-2 xl:max-w-7xl xl:gap-x-8'
|
||||||
)}
|
)}
|
||||||
style={suspend ? visuallyHiddenStyle : undefined}
|
|
||||||
>
|
>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:block" />
|
<Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" />
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'lg:col-span-8 lg:pt-6',
|
'lg:col-span-8 lg:pt-6',
|
||||||
|
@ -46,22 +43,7 @@ export function Page(props: {
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BottomNavBar />
|
<BottomNavBar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const visuallyHiddenStyle = {
|
|
||||||
clip: 'rect(0 0 0 0)',
|
|
||||||
clipPath: 'inset(50%)',
|
|
||||||
height: 1,
|
|
||||||
margin: -1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
width: 1,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
userSelect: 'none',
|
|
||||||
visibility: 'hidden',
|
|
||||||
} as const
|
|
||||||
|
|
|
@ -1,6 +1,40 @@
|
||||||
|
import { ReactNode } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
|
export function PaginationNextPrev(props: {
|
||||||
|
className?: string
|
||||||
|
prev?: ReactNode
|
||||||
|
next?: ReactNode
|
||||||
|
onClickPrev: () => void
|
||||||
|
onClickNext: () => void
|
||||||
|
scrollToTop?: boolean
|
||||||
|
}) {
|
||||||
|
const { className, prev, next, onClickPrev, onClickNext, scrollToTop } = props
|
||||||
|
return (
|
||||||
|
<Row className={clsx(className, 'flex-1 justify-between sm:justify-end')}>
|
||||||
|
{prev != null && (
|
||||||
|
<a
|
||||||
|
href={scrollToTop ? '#' : undefined}
|
||||||
|
className="relative inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
onClick={onClickPrev}
|
||||||
|
>
|
||||||
|
{prev ?? 'Previous'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{next != null && (
|
||||||
|
<a
|
||||||
|
href={scrollToTop ? '#' : undefined}
|
||||||
|
className="relative ml-3 inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
onClick={onClickNext}
|
||||||
|
>
|
||||||
|
{next ?? 'Next'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
export function Pagination(props: {
|
export function Pagination(props: {
|
||||||
page: number
|
page: number
|
||||||
itemsPerPage: number
|
itemsPerPage: number
|
||||||
|
@ -44,24 +78,13 @@ export function Pagination(props: {
|
||||||
of <span className="font-medium">{totalItems}</span> results
|
of <span className="font-medium">{totalItems}</span> results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 justify-between sm:justify-end">
|
<PaginationNextPrev
|
||||||
{page > 0 && (
|
prev={page > 0 ? prevTitle ?? 'Previous' : null}
|
||||||
<a
|
next={page < maxPage ? nextTitle ?? 'Next' : null}
|
||||||
href={scrollToTop ? '#' : undefined}
|
onClickPrev={() => setPage(page - 1)}
|
||||||
className="relative inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
onClickNext={() => setPage(page + 1)}
|
||||||
onClick={() => page > 0 && setPage(page - 1)}
|
scrollToTop={scrollToTop}
|
||||||
>
|
/>
|
||||||
{prevTitle ?? 'Previous'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href={scrollToTop ? '#' : undefined}
|
|
||||||
className="relative ml-3 inline-flex cursor-pointer select-none items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
||||||
onClick={() => page < maxPage && setPage(page + 1)}
|
|
||||||
>
|
|
||||||
{nextTitle ?? 'Next'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { ResponsiveLine } from '@nivo/line'
|
import { ResponsiveLine } from '@nivo/line'
|
||||||
import { PortfolioMetrics } from 'common/user'
|
import { PortfolioMetrics } from 'common/user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { DAY_MS } from 'common/util/time'
|
|
||||||
import { last } from 'lodash'
|
import { last } from 'lodash'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
@ -10,28 +9,12 @@ import { formatTime } from 'web/lib/util/time'
|
||||||
export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
||||||
portfolioHistory: PortfolioMetrics[]
|
portfolioHistory: PortfolioMetrics[]
|
||||||
height?: number
|
height?: number
|
||||||
period?: string
|
includeTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { portfolioHistory, height, period } = props
|
const { portfolioHistory, height, includeTime } = props
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
const portfolioHistoryFiltered = portfolioHistory.filter((p) => {
|
const points = portfolioHistory.map((p) => {
|
||||||
switch (period) {
|
|
||||||
case 'daily':
|
|
||||||
return p.timestamp > Date.now() - 1 * DAY_MS
|
|
||||||
case 'weekly':
|
|
||||||
return p.timestamp > Date.now() - 7 * DAY_MS
|
|
||||||
case 'monthly':
|
|
||||||
return p.timestamp > Date.now() - 30 * DAY_MS
|
|
||||||
case 'allTime':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const points = portfolioHistoryFiltered.map((p) => {
|
|
||||||
return {
|
return {
|
||||||
x: new Date(p.timestamp),
|
x: new Date(p.timestamp),
|
||||||
y: p.balance + p.investmentValue,
|
y: p.balance + p.investmentValue,
|
||||||
|
@ -41,7 +24,6 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
||||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||||
const numYTickValues = 4
|
const numYTickValues = 4
|
||||||
const endDate = last(points)?.x
|
const endDate = last(points)?.x
|
||||||
const includeTime = period === 'daily'
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-hidden"
|
className="w-full overflow-hidden"
|
||||||
|
@ -66,7 +48,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
||||||
colors={{ datum: 'color' }}
|
colors={{ datum: 'color' }}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
tickValues: numXTickValues,
|
tickValues: numXTickValues,
|
||||||
format: (time) => formatTime(+time, includeTime),
|
format: (time) => formatTime(+time, !!includeTime),
|
||||||
}}
|
}}
|
||||||
pointBorderColor="#fff"
|
pointBorderColor="#fff"
|
||||||
pointSize={points.length > 100 ? 0 : 6}
|
pointSize={points.length > 100 ? 0 : 6}
|
||||||
|
|
|
@ -1,75 +1,56 @@
|
||||||
import { PortfolioMetrics } from 'common/user'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { last } from 'lodash'
|
import { last } from 'lodash'
|
||||||
import { memo, useEffect, useState } from 'react'
|
import { memo, useRef, useState } from 'react'
|
||||||
import { Period, getPortfolioHistory } from 'web/lib/firebase/users'
|
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||||
|
import { Period } from 'web/lib/firebase/users'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { PortfolioValueGraph } from './portfolio-value-graph'
|
import { PortfolioValueGraph } from './portfolio-value-graph'
|
||||||
|
|
||||||
export const PortfolioValueSection = memo(
|
export const PortfolioValueSection = memo(
|
||||||
function PortfolioValueSection(props: {
|
function PortfolioValueSection(props: { userId: string }) {
|
||||||
userId: string
|
const { userId } = props
|
||||||
disableSelector?: boolean
|
|
||||||
}) {
|
|
||||||
const { disableSelector, userId } = props
|
|
||||||
|
|
||||||
const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('weekly')
|
const [portfolioPeriod, setPortfolioPeriod] = useState<Period>('weekly')
|
||||||
const [portfolioHistory, setUsersPortfolioHistory] = useState<
|
const portfolioHistory = usePortfolioHistory(userId, portfolioPeriod)
|
||||||
PortfolioMetrics[]
|
|
||||||
>([])
|
|
||||||
useEffect(() => {
|
|
||||||
getPortfolioHistory(userId).then(setUsersPortfolioHistory)
|
|
||||||
}, [userId])
|
|
||||||
const lastPortfolioMetrics = last(portfolioHistory)
|
|
||||||
|
|
||||||
if (portfolioHistory.length === 0 || !lastPortfolioMetrics) {
|
// Remember the last defined portfolio history.
|
||||||
|
const portfolioRef = useRef(portfolioHistory)
|
||||||
|
if (portfolioHistory) portfolioRef.current = portfolioHistory
|
||||||
|
const currPortfolioHistory = portfolioRef.current
|
||||||
|
|
||||||
|
const lastPortfolioMetrics = last(currPortfolioHistory)
|
||||||
|
if (!currPortfolioHistory || !lastPortfolioMetrics) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: If portfolio history started on June 1st, then we label it as "Since June"
|
const { balance, investmentValue } = lastPortfolioMetrics
|
||||||
// instead of "All time"
|
const totalValue = balance + investmentValue
|
||||||
const allTimeLabel =
|
|
||||||
lastPortfolioMetrics.timestamp < Date.parse('2022-06-20T00:00:00.000Z')
|
|
||||||
? 'Since June'
|
|
||||||
: 'All time'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Row className="gap-8">
|
<Row className="gap-8">
|
||||||
<div className="mb-4 w-full">
|
<Col className="flex-1 justify-center">
|
||||||
<Col
|
<div className="text-sm text-gray-500">Portfolio value</div>
|
||||||
className={disableSelector ? 'items-center justify-center' : ''}
|
<div className="text-lg">{formatMoney(totalValue)}</div>
|
||||||
>
|
</Col>
|
||||||
<div className="text-sm text-gray-500">Portfolio value</div>
|
<select
|
||||||
<div className="text-lg">
|
className="select select-bordered self-start"
|
||||||
{formatMoney(
|
value={portfolioPeriod}
|
||||||
lastPortfolioMetrics.balance +
|
onChange={(e) => {
|
||||||
lastPortfolioMetrics.investmentValue
|
setPortfolioPeriod(e.target.value as Period)
|
||||||
)}
|
}}
|
||||||
</div>
|
>
|
||||||
</Col>
|
<option value="allTime">All time</option>
|
||||||
</div>
|
<option value="weekly">Last 7d</option>
|
||||||
{!disableSelector && (
|
<option value="daily">Last 24h</option>
|
||||||
<select
|
</select>
|
||||||
className="select select-bordered self-start"
|
|
||||||
value={portfolioPeriod}
|
|
||||||
onChange={(e) => {
|
|
||||||
setPortfolioPeriod(e.target.value as Period)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="allTime">{allTimeLabel}</option>
|
|
||||||
<option value="weekly">Last 7d</option>
|
|
||||||
{/* Note: 'daily' seems to be broken? */}
|
|
||||||
{/* <option value="daily">Last 24h</option> */}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
<PortfolioValueGraph
|
<PortfolioValueGraph
|
||||||
portfolioHistory={portfolioHistory}
|
portfolioHistory={currPortfolioHistory}
|
||||||
period={portfolioPeriod}
|
includeTime={portfolioPeriod == 'daily'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function LoansModal(props: {
|
||||||
<Col className={'gap-2'}>
|
<Col className={'gap-2'}>
|
||||||
<span className={'text-indigo-700'}>• What are daily loans?</span>
|
<span className={'text-indigo-700'}>• What are daily loans?</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
Every day at midnight PT, get 1% of your total bet amount back as a
|
Every day at midnight PT, get 2% of your total bet amount back as a
|
||||||
loan.
|
loan.
|
||||||
</span>
|
</span>
|
||||||
<span className={'text-indigo-700'}>
|
<span className={'text-indigo-700'}>
|
||||||
|
@ -34,12 +34,12 @@ export function LoansModal(props: {
|
||||||
</span>
|
</span>
|
||||||
<span className={'text-indigo-700'}>• What is an example?</span>
|
<span className={'text-indigo-700'}>• What is an example?</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
For example, if you bet M$1000 on "Will I become a millionare?" on
|
For example, if you bet M$1000 on "Will I become a millionare?"
|
||||||
Monday, you will get M$10 back on Tuesday.
|
today, you will get M$20 back tomorrow.
|
||||||
</span>
|
</span>
|
||||||
<span className={'ml-2'}>
|
<span className={'ml-2'}>
|
||||||
Previous loans count against your total bet amount. So on Wednesday,
|
Previous loans count against your total bet amount. So on the next
|
||||||
you would get back 1% of M$990 = M$9.9.
|
day, you would get back 2% of M$(1000 - 20) = M$19.6.
|
||||||
</span>
|
</span>
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function RelativeTimestamp(props: { time: number }) {
|
||||||
return (
|
return (
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip
|
||||||
className="ml-1 whitespace-nowrap text-gray-400"
|
className="ml-1 whitespace-nowrap text-gray-400"
|
||||||
time={dayJsTime}
|
time={time}
|
||||||
>
|
>
|
||||||
{dayJsTime.fromNow()}
|
{dayJsTime.fromNow()}
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
|
|
46
web/components/share-post-modal.tsx
Normal file
46
web/components/share-post-modal.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { LinkIcon } from '@heroicons/react/outline'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Title } from './title'
|
||||||
|
import { Button } from './button'
|
||||||
|
import { TweetButton } from './tweet-button'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
|
export function SharePostModal(props: {
|
||||||
|
shareUrl: string
|
||||||
|
isOpen: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { isOpen, setOpen, shareUrl } = props
|
||||||
|
|
||||||
|
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} setOpen={setOpen} size="md">
|
||||||
|
<Col className="gap-4 rounded bg-white p-4">
|
||||||
|
<Title className="!mt-0 !mb-2" text="Share this post" />
|
||||||
|
<Button
|
||||||
|
size="2xl"
|
||||||
|
color="gradient"
|
||||||
|
className={'mb-2 flex max-w-xs self-center'}
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(shareUrl)
|
||||||
|
toast.success('Link copied!', {
|
||||||
|
icon: linkIcon,
|
||||||
|
})
|
||||||
|
track('copy share post link')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{linkIcon} Copy link
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Row className="z-0 justify-start gap-4 self-center">
|
||||||
|
<TweetButton className="self-start" tweetText={shareUrl} />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
25
web/components/sign-in-button.tsx
Normal file
25
web/components/sign-in-button.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
|
import { Button } from './button'
|
||||||
|
|
||||||
|
export const SignInButton = (props: { className?: string }) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
color="gray"
|
||||||
|
onClick={async () => {
|
||||||
|
// login, and then reload the page, to hit any SSR redirect (e.g.
|
||||||
|
// redirecting from / to /home for logged in users)
|
||||||
|
await firebaseLogin()
|
||||||
|
router.replace(router.asPath)
|
||||||
|
}}
|
||||||
|
className={props.className}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { Button, SizeType } from './button'
|
import { Button, SizeType } from './button'
|
||||||
|
|
||||||
export function SignUpPrompt(props: {
|
export function BetSignUpPrompt(props: {
|
||||||
label?: string
|
label?: string
|
||||||
className?: string
|
className?: string
|
||||||
size?: SizeType
|
size?: SizeType
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ReactNode } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export const linkClass =
|
export const linkClass =
|
||||||
'z-10 break-words hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
'z-10 break-anywhere hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||||
|
|
||||||
export const SiteLink = (props: {
|
export const SiteLink = (props: {
|
||||||
href: string
|
href: string
|
||||||
|
@ -19,7 +19,6 @@ export const SiteLink = (props: {
|
||||||
className={clsx(linkClass, className)}
|
className={clsx(linkClass, className)}
|
||||||
href={href}
|
href={href}
|
||||||
target={href.startsWith('http') ? '_blank' : undefined}
|
target={href.startsWith('http') ? '_blank' : undefined}
|
||||||
style={{ /* For iOS safari */ wordBreak: 'break-word' }}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (onClick) onClick()
|
if (onClick) onClick()
|
||||||
|
|
|
@ -10,7 +10,7 @@ export function ToastClipboard(props: { className?: string }) {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'border-base-300 absolute items-center' +
|
'border-base-300 absolute items-center' +
|
||||||
'gap-2 divide-x divide-gray-200 rounded-md border-2 bg-white ' +
|
'gap-2 divide-x divide-gray-200 rounded-md border-2 bg-white ' +
|
||||||
'h-15 w-[15rem] p-2 pr-3 text-gray-500',
|
'h-15 z-10 w-[15rem] p-2 pr-3 text-gray-500',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
useRole,
|
useRole,
|
||||||
} from '@floating-ui/react-dom-interactions'
|
} from '@floating-ui/react-dom-interactions'
|
||||||
import { Transition } from '@headlessui/react'
|
import { Transition } from '@headlessui/react'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { ReactNode, useRef, useState } from 'react'
|
import { ReactNode, useRef, useState } from 'react'
|
||||||
|
|
||||||
// See https://floating-ui.com/docs/react-dom
|
// See https://floating-ui.com/docs/react-dom
|
||||||
|
@ -58,14 +57,10 @@ export function Tooltip(props: {
|
||||||
}[placement.split('-')[0]] as string
|
}[placement.split('-')[0]] as string
|
||||||
|
|
||||||
return text ? (
|
return text ? (
|
||||||
<div className="contents">
|
<>
|
||||||
<div
|
<span className={className} ref={reference} {...getReferenceProps()}>
|
||||||
className={clsx('inline-block', className)}
|
|
||||||
ref={reference}
|
|
||||||
{...getReferenceProps()}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</span>
|
||||||
{/* conditionally render tooltip and fade in/out */}
|
{/* conditionally render tooltip and fade in/out */}
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
|
@ -95,7 +90,7 @@ export function Tooltip(props: {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -119,10 +119,7 @@ export function UserPage(props: { user: User }) {
|
||||||
<Col className="mx-4 -mt-6">
|
<Col className="mx-4 -mt-6">
|
||||||
<Row className={'flex-wrap justify-between gap-y-2'}>
|
<Row className={'flex-wrap justify-between gap-y-2'}>
|
||||||
<Col>
|
<Col>
|
||||||
<span
|
<span className="break-anywhere text-2xl font-bold">
|
||||||
className="text-2xl font-bold"
|
|
||||||
style={{ wordBreak: 'break-word' }}
|
|
||||||
>
|
|
||||||
{user.name}
|
{user.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-500">@{user.username}</span>
|
<span className="text-gray-500">@{user.username}</span>
|
||||||
|
|
|
@ -5,13 +5,19 @@ import clsx from 'clsx'
|
||||||
export default function ShortToggle(props: {
|
export default function ShortToggle(props: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
setEnabled: (enabled: boolean) => void
|
setEnabled: (enabled: boolean) => void
|
||||||
|
onChange?: (enabled: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const { enabled, setEnabled } = props
|
const { enabled, setEnabled } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onChange={setEnabled}
|
onChange={(e: boolean) => {
|
||||||
|
setEnabled(e)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
className="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
import { isEqual } from 'lodash'
|
import { isEqual } from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
listenForHotContracts,
|
listenForHotContracts,
|
||||||
listenForInactiveContracts,
|
listenForInactiveContracts,
|
||||||
listenForNewContracts,
|
listenForNewContracts,
|
||||||
|
getUserBetContractsQuery,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
export const useContracts = () => {
|
export const useContracts = () => {
|
||||||
|
@ -89,3 +91,15 @@ export const useUpdatedContracts = (contracts: Contract[] | undefined) => {
|
||||||
? contracts.map((c) => contractDict.current[c.id])
|
? contracts.map((c) => contractDict.current[c.id])
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUserBetContracts = (userId: string) => {
|
||||||
|
const result = useFirestoreQueryData(
|
||||||
|
['contracts', 'bets', userId],
|
||||||
|
getUserBetContractsQuery(userId),
|
||||||
|
{ subscribe: true, includeMetadataChanges: true },
|
||||||
|
// Temporary workaround for react-query bug:
|
||||||
|
// https://github.com/invertase/react-query-firebase/issues/25
|
||||||
|
{ refetchOnMount: 'always' }
|
||||||
|
)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { listenForFollowers, listenForFollows } from 'web/lib/firebase/users'
|
import { listenForFollowers, listenForFollows } from 'web/lib/firebase/users'
|
||||||
import { contracts, listenForContractFollows } from 'web/lib/firebase/contracts'
|
import { listenForContractFollows } from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
export const useFollows = (userId: string | null | undefined) => {
|
export const useFollows = (userId: string | null | undefined) => {
|
||||||
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
||||||
import { Notification } from 'common/notification'
|
import { Notification } from 'common/notification'
|
||||||
import {
|
import { getNotificationsQuery } from 'web/lib/firebase/notifications'
|
||||||
getNotificationsQuery,
|
|
||||||
listenForNotifications,
|
|
||||||
} from 'web/lib/firebase/notifications'
|
|
||||||
import { groupBy, map, partition } from 'lodash'
|
import { groupBy, map, partition } from 'lodash'
|
||||||
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
|
||||||
|
|
||||||
export type NotificationGroup = {
|
export type NotificationGroup = {
|
||||||
notifications: Notification[]
|
notifications: Notification[]
|
||||||
|
@ -17,49 +13,49 @@ export type NotificationGroup = {
|
||||||
type: 'income' | 'normal'
|
type: 'income' | 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
// For some reason react-query subscriptions don't actually listen for notifications
|
function useNotifications(privateUser: PrivateUser) {
|
||||||
// Use useUnseenPreferredNotificationGroups to listen for new notifications
|
|
||||||
export function usePreferredGroupedNotifications(
|
|
||||||
privateUser: PrivateUser,
|
|
||||||
cachedNotifications?: Notification[]
|
|
||||||
) {
|
|
||||||
const result = useFirestoreQueryData(
|
const result = useFirestoreQueryData(
|
||||||
['notifications-all', privateUser.id],
|
['notifications-all', privateUser.id],
|
||||||
getNotificationsQuery(privateUser.id)
|
getNotificationsQuery(privateUser.id),
|
||||||
|
{ subscribe: true, includeMetadataChanges: true },
|
||||||
|
// Temporary workaround for react-query bug:
|
||||||
|
// https://github.com/invertase/react-query-firebase/issues/25
|
||||||
|
{ refetchOnMount: 'always' }
|
||||||
)
|
)
|
||||||
|
|
||||||
const notifications = useMemo(() => {
|
const notifications = useMemo(() => {
|
||||||
if (result.isLoading) return cachedNotifications ?? []
|
if (!result.data) return undefined
|
||||||
if (!result.data) return cachedNotifications ?? []
|
|
||||||
const notifications = result.data as Notification[]
|
const notifications = result.data as Notification[]
|
||||||
|
|
||||||
return getAppropriateNotifications(
|
return getAppropriateNotifications(
|
||||||
notifications,
|
notifications,
|
||||||
privateUser.notificationPreferences
|
privateUser.notificationPreferences
|
||||||
).filter((n) => !n.isSeenOnHref)
|
).filter((n) => !n.isSeenOnHref)
|
||||||
}, [
|
}, [privateUser.notificationPreferences, result.data])
|
||||||
cachedNotifications,
|
|
||||||
privateUser.notificationPreferences,
|
|
||||||
result.data,
|
|
||||||
result.isLoading,
|
|
||||||
])
|
|
||||||
|
|
||||||
|
return notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnseenNotifications(privateUser: PrivateUser) {
|
||||||
|
const notifications = useNotifications(privateUser)
|
||||||
|
return useMemo(
|
||||||
|
() => notifications && notifications.filter((n) => !n.isSeen),
|
||||||
|
[notifications]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGroupedNotifications(privateUser: PrivateUser) {
|
||||||
|
const notifications = useNotifications(privateUser)
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (notifications) return groupNotifications(notifications)
|
if (notifications) return groupNotifications(notifications)
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUnseenPreferredNotificationGroups(privateUser: PrivateUser) {
|
export function useUnseenGroupedNotification(privateUser: PrivateUser) {
|
||||||
const notifications = useUnseenPreferredNotifications(privateUser, {})
|
const notifications = useUnseenNotifications(privateUser)
|
||||||
const [notificationGroups, setNotificationGroups] = useState<
|
return useMemo(() => {
|
||||||
NotificationGroup[] | undefined
|
if (notifications) return groupNotifications(notifications)
|
||||||
>(undefined)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!notifications) return
|
|
||||||
|
|
||||||
const groupedNotifications = groupNotifications(notifications)
|
|
||||||
setNotificationGroups(groupedNotifications)
|
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
return notificationGroups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupNotifications(notifications: Notification[]) {
|
export function groupNotifications(notifications: Notification[]) {
|
||||||
|
@ -120,36 +116,6 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
return notificationGroups
|
return notificationGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUnseenPreferredNotifications(
|
|
||||||
privateUser: PrivateUser,
|
|
||||||
options: { customHref?: string },
|
|
||||||
limit: number = NOTIFICATIONS_PER_PAGE
|
|
||||||
) {
|
|
||||||
const { customHref } = options
|
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
||||||
const [userAppropriateNotifications, setUserAppropriateNotifications] =
|
|
||||||
useState<Notification[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return listenForNotifications(privateUser.id, setNotifications, {
|
|
||||||
unseenOnly: true,
|
|
||||||
limit,
|
|
||||||
})
|
|
||||||
}, [limit, privateUser.id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const notificationsToShow = getAppropriateNotifications(
|
|
||||||
notifications,
|
|
||||||
privateUser.notificationPreferences
|
|
||||||
).filter((n) =>
|
|
||||||
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
|
|
||||||
)
|
|
||||||
setUserAppropriateNotifications(notificationsToShow)
|
|
||||||
}, [notifications, customHref, privateUser.notificationPreferences])
|
|
||||||
|
|
||||||
return userAppropriateNotifications
|
|
||||||
}
|
|
||||||
|
|
||||||
const lessPriorityReasons = [
|
const lessPriorityReasons = [
|
||||||
'on_contract_with_users_comment',
|
'on_contract_with_users_comment',
|
||||||
'on_contract_with_users_answer',
|
'on_contract_with_users_answer',
|
||||||
|
|
109
web/hooks/use-pagination.ts
Normal file
109
web/hooks/use-pagination.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// adapted from https://github.com/premshree/use-pagination-firestore
|
||||||
|
|
||||||
|
import { useEffect, useReducer } from 'react'
|
||||||
|
import {
|
||||||
|
Query,
|
||||||
|
QuerySnapshot,
|
||||||
|
QueryDocumentSnapshot,
|
||||||
|
queryEqual,
|
||||||
|
limit,
|
||||||
|
onSnapshot,
|
||||||
|
query,
|
||||||
|
startAfter,
|
||||||
|
} from 'firebase/firestore'
|
||||||
|
|
||||||
|
interface State<T> {
|
||||||
|
baseQ: Query<T>
|
||||||
|
docs: QueryDocumentSnapshot<T>[]
|
||||||
|
pageStart: number
|
||||||
|
pageEnd: number
|
||||||
|
pageSize: number
|
||||||
|
isLoading: boolean
|
||||||
|
isComplete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionBase<K, V = void> = V extends void ? { type: K } : { type: K } & V
|
||||||
|
|
||||||
|
type Action<T> =
|
||||||
|
| ActionBase<'INIT', { opts: PaginationOptions<T> }>
|
||||||
|
| ActionBase<'LOAD', { snapshot: QuerySnapshot<T> }>
|
||||||
|
| ActionBase<'PREV'>
|
||||||
|
| ActionBase<'NEXT'>
|
||||||
|
|
||||||
|
const getReducer =
|
||||||
|
<T>() =>
|
||||||
|
(state: State<T>, action: Action<T>): State<T> => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INIT': {
|
||||||
|
return getInitialState(action.opts)
|
||||||
|
}
|
||||||
|
case 'LOAD': {
|
||||||
|
const docs = state.docs.concat(action.snapshot.docs)
|
||||||
|
const isComplete = action.snapshot.docs.length < state.pageSize
|
||||||
|
return { ...state, docs, isComplete, isLoading: false }
|
||||||
|
}
|
||||||
|
case 'PREV': {
|
||||||
|
const { pageStart, pageSize } = state
|
||||||
|
const prevStart = pageStart - pageSize
|
||||||
|
const isLoading = false
|
||||||
|
return { ...state, isLoading, pageStart: prevStart, pageEnd: pageStart }
|
||||||
|
}
|
||||||
|
case 'NEXT': {
|
||||||
|
const { docs, pageEnd, isComplete, pageSize } = state
|
||||||
|
const nextEnd = pageEnd + pageSize
|
||||||
|
const isLoading = !isComplete && docs.length < nextEnd
|
||||||
|
return { ...state, isLoading, pageStart: pageEnd, pageEnd: nextEnd }
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid action.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaginationOptions<T> = { q: Query<T>; pageSize: number }
|
||||||
|
|
||||||
|
const getInitialState = <T>(opts: PaginationOptions<T>): State<T> => {
|
||||||
|
return {
|
||||||
|
baseQ: opts.q,
|
||||||
|
docs: [],
|
||||||
|
pageStart: 0,
|
||||||
|
pageEnd: opts.pageSize,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
isLoading: true,
|
||||||
|
isComplete: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePagination = <T>(opts: PaginationOptions<T>) => {
|
||||||
|
const [state, dispatch] = useReducer(getReducer<T>(), opts, getInitialState)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// save callers the effort of ref-izing their opts by checking for
|
||||||
|
// deep equality over here
|
||||||
|
if (queryEqual(opts.q, state.baseQ) && opts.pageSize === state.pageSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dispatch({ type: 'INIT', opts })
|
||||||
|
}, [opts, state.baseQ, state.pageSize])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.isLoading) {
|
||||||
|
const lastDoc = state.docs[state.docs.length - 1]
|
||||||
|
const nextQ = lastDoc
|
||||||
|
? query(state.baseQ, startAfter(lastDoc), limit(state.pageSize))
|
||||||
|
: query(state.baseQ, limit(state.pageSize))
|
||||||
|
return onSnapshot(nextQ, (snapshot) => {
|
||||||
|
dispatch({ type: 'LOAD', snapshot })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [state.isLoading, state.baseQ, state.docs, state.pageSize])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
isStart: state.pageStart === 0,
|
||||||
|
isEnd: state.isComplete && state.pageEnd >= state.docs.length,
|
||||||
|
getPrev: () => dispatch({ type: 'PREV' }),
|
||||||
|
getNext: () => dispatch({ type: 'NEXT' }),
|
||||||
|
getItems: () =>
|
||||||
|
state.docs.slice(state.pageStart, state.pageEnd).map((d) => d.data()),
|
||||||
|
}
|
||||||
|
}
|
32
web/hooks/use-portfolio-history.ts
Normal file
32
web/hooks/use-portfolio-history.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
|
import { DAY_MS, HOUR_MS } from 'common/util/time'
|
||||||
|
import { getPortfolioHistoryQuery, Period } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export const usePortfolioHistory = (userId: string, period: Period) => {
|
||||||
|
const nowRounded = Math.round(Date.now() / HOUR_MS) * HOUR_MS
|
||||||
|
const cutoff = periodToCutoff(nowRounded, period).valueOf()
|
||||||
|
|
||||||
|
const result = useFirestoreQueryData(
|
||||||
|
['portfolio-history', userId, cutoff],
|
||||||
|
getPortfolioHistoryQuery(userId, cutoff),
|
||||||
|
{ subscribe: true, includeMetadataChanges: true },
|
||||||
|
// Temporary workaround for react-query bug:
|
||||||
|
// https://github.com/invertase/react-query-firebase/issues/25
|
||||||
|
{ refetchOnMount: 'always' }
|
||||||
|
)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodToCutoff = (now: number, period: Period) => {
|
||||||
|
switch (period) {
|
||||||
|
case 'daily':
|
||||||
|
return now - 1 * DAY_MS
|
||||||
|
case 'weekly':
|
||||||
|
return now - 7 * DAY_MS
|
||||||
|
case 'monthly':
|
||||||
|
return now - 30 * DAY_MS
|
||||||
|
case 'allTime':
|
||||||
|
default:
|
||||||
|
return new Date(0)
|
||||||
|
}
|
||||||
|
}
|
11
web/hooks/use-prefetch.ts
Normal file
11
web/hooks/use-prefetch.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useUserBetContracts } from './use-contracts'
|
||||||
|
import { usePortfolioHistory } from './use-portfolio-history'
|
||||||
|
import { useUserBets } from './use-user-bets'
|
||||||
|
|
||||||
|
export function usePrefetch(userId: string | undefined) {
|
||||||
|
const maybeUserId = userId ?? ''
|
||||||
|
|
||||||
|
useUserBets(maybeUserId)
|
||||||
|
useUserBetContracts(maybeUserId)
|
||||||
|
usePortfolioHistory(maybeUserId, 'weekly')
|
||||||
|
}
|
|
@ -1,22 +1,21 @@
|
||||||
import { uniq } from 'lodash'
|
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Bet,
|
Bet,
|
||||||
listenForUserBets,
|
getUserBetsQuery,
|
||||||
listenForUserContractBets,
|
listenForUserContractBets,
|
||||||
} from 'web/lib/firebase/bets'
|
} from 'web/lib/firebase/bets'
|
||||||
|
|
||||||
export const useUserBets = (
|
export const useUserBets = (userId: string) => {
|
||||||
userId: string | undefined,
|
const result = useFirestoreQueryData(
|
||||||
options: { includeRedemptions: boolean }
|
['bets', userId],
|
||||||
) => {
|
getUserBetsQuery(userId),
|
||||||
const [bets, setBets] = useState<Bet[] | undefined>(undefined)
|
{ subscribe: true, includeMetadataChanges: true },
|
||||||
|
// Temporary workaround for react-query bug:
|
||||||
useEffect(() => {
|
// https://github.com/invertase/react-query-firebase/issues/25
|
||||||
if (userId) return listenForUserBets(userId, setBets, options)
|
{ refetchOnMount: 'always' }
|
||||||
}, [userId])
|
)
|
||||||
|
return result.data
|
||||||
return bets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserContractBets = (
|
export const useUserContractBets = (
|
||||||
|
@ -33,36 +32,6 @@ export const useUserContractBets = (
|
||||||
return bets
|
return bets
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserBetContracts = (
|
|
||||||
userId: string | undefined,
|
|
||||||
options: { includeRedemptions: boolean }
|
|
||||||
) => {
|
|
||||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId) {
|
|
||||||
const key = `user-bet-contractIds-${userId}`
|
|
||||||
|
|
||||||
const userBetContractJson = localStorage.getItem(key)
|
|
||||||
if (userBetContractJson) {
|
|
||||||
setContractIds(JSON.parse(userBetContractJson))
|
|
||||||
}
|
|
||||||
|
|
||||||
return listenForUserBets(
|
|
||||||
userId,
|
|
||||||
(bets) => {
|
|
||||||
const contractIds = uniq(bets.map((bet) => bet.contractId))
|
|
||||||
setContractIds(contractIds)
|
|
||||||
localStorage.setItem(key, JSON.stringify(contractIds))
|
|
||||||
},
|
|
||||||
options
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [userId])
|
|
||||||
|
|
||||||
return contractIds
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetUserBetContractIds = (userId: string | undefined) => {
|
export const useGetUserBetContractIds = (userId: string | undefined) => {
|
||||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { auth } from './users'
|
import { auth } from './users'
|
||||||
import { APIError, getFunctionUrl } from 'common/api'
|
import { APIError, getFunctionUrl } from 'common/api'
|
||||||
|
import { JSONContent } from '@tiptap/core'
|
||||||
export { APIError } from 'common/api'
|
export { APIError } from 'common/api'
|
||||||
|
|
||||||
export async function call(url: string, method: string, params: any) {
|
export async function call(url: string, method: string, params: any) {
|
||||||
|
@ -88,3 +89,7 @@ export function acceptChallenge(params: any) {
|
||||||
export function getCurrentUser(params: any) {
|
export function getCurrentUser(params: any) {
|
||||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createPost(params: { title: string; content: JSONContent }) {
|
||||||
|
return call(getFunctionUrl('createpost'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
getDocs,
|
getDocs,
|
||||||
getDoc,
|
getDoc,
|
||||||
DocumentSnapshot,
|
DocumentSnapshot,
|
||||||
|
Query,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
|
@ -131,24 +132,12 @@ export async function getContractsOfUserBets(userId: string) {
|
||||||
return filterDefined(contracts)
|
return filterDefined(contracts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForUserBets(
|
export function getUserBetsQuery(userId: string) {
|
||||||
userId: string,
|
return query(
|
||||||
setBets: (bets: Bet[]) => void,
|
|
||||||
options: { includeRedemptions: boolean }
|
|
||||||
) {
|
|
||||||
const { includeRedemptions } = options
|
|
||||||
const userQuery = query(
|
|
||||||
collectionGroup(db, 'bets'),
|
collectionGroup(db, 'bets'),
|
||||||
where('userId', '==', userId),
|
where('userId', '==', userId),
|
||||||
orderBy('createdTime', 'desc')
|
orderBy('createdTime', 'desc')
|
||||||
)
|
) as Query<Bet>
|
||||||
return listenForValues<Bet>(userQuery, (bets) => {
|
|
||||||
setBets(
|
|
||||||
bets.filter(
|
|
||||||
(bet) => (includeRedemptions || !bet.isRedemption) && !bet.isAnte
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForUserContractBets(
|
export function listenForUserContractBets(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Query,
|
||||||
collection,
|
collection,
|
||||||
collectionGroup,
|
collectionGroup,
|
||||||
doc,
|
doc,
|
||||||
|
@ -148,12 +149,10 @@ export function listenForRecentComments(
|
||||||
return listenForValues<Comment>(recentCommentsQuery, setComments)
|
return listenForValues<Comment>(recentCommentsQuery, setComments)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsersCommentsQuery = (userId: string) =>
|
export const getUserCommentsQuery = (userId: string) =>
|
||||||
query(
|
query(
|
||||||
collectionGroup(db, 'comments'),
|
collectionGroup(db, 'comments'),
|
||||||
where('userId', '==', userId),
|
where('userId', '==', userId),
|
||||||
|
where('commentType', '==', 'contract'),
|
||||||
orderBy('createdTime', 'desc')
|
orderBy('createdTime', 'desc')
|
||||||
)
|
) as Query<ContractComment>
|
||||||
export async function getUsersComments(userId: string) {
|
|
||||||
return await getValues<Comment>(getUsersCommentsQuery(userId))
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,13 +6,14 @@ import {
|
||||||
getDocs,
|
getDocs,
|
||||||
limit,
|
limit,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
Query,
|
||||||
query,
|
query,
|
||||||
setDoc,
|
setDoc,
|
||||||
startAfter,
|
startAfter,
|
||||||
updateDoc,
|
updateDoc,
|
||||||
where,
|
where,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { sortBy, sum } from 'lodash'
|
import { sortBy, sum, uniqBy } from 'lodash'
|
||||||
|
|
||||||
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
||||||
import { BinaryContract, Contract } from 'common/contract'
|
import { BinaryContract, Contract } from 'common/contract'
|
||||||
|
@ -156,6 +157,13 @@ export function listenForUserContracts(
|
||||||
return listenForValues<Contract>(q, setContracts)
|
return listenForValues<Contract>(q, setContracts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserBetContractsQuery(userId: string) {
|
||||||
|
return query(
|
||||||
|
contracts,
|
||||||
|
where('uniqueBettorIds', 'array-contains', userId)
|
||||||
|
) as Query<Contract>
|
||||||
|
}
|
||||||
|
|
||||||
const activeContractsQuery = query(
|
const activeContractsQuery = query(
|
||||||
contracts,
|
contracts,
|
||||||
where('isResolved', '==', false),
|
where('isResolved', '==', false),
|
||||||
|
@ -305,7 +313,7 @@ export const getRandTopCreatorContracts = async (
|
||||||
where('isResolved', '==', false),
|
where('isResolved', '==', false),
|
||||||
where('creatorId', '==', creatorId),
|
where('creatorId', '==', creatorId),
|
||||||
orderBy('popularityScore', 'desc'),
|
orderBy('popularityScore', 'desc'),
|
||||||
limit(Math.max(count * 2, 15))
|
limit(count * 2)
|
||||||
)
|
)
|
||||||
const data = await getValues<Contract>(creatorContractsQuery)
|
const data = await getValues<Contract>(creatorContractsQuery)
|
||||||
const open = data
|
const open = data
|
||||||
|
@ -315,6 +323,44 @@ export const getRandTopCreatorContracts = async (
|
||||||
return chooseRandomSubset(open, count)
|
return chooseRandomSubset(open, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRandTopGroupContracts = async (
|
||||||
|
groupSlug: string,
|
||||||
|
count: number,
|
||||||
|
excluding: string[] = []
|
||||||
|
) => {
|
||||||
|
const creatorContractsQuery = query(
|
||||||
|
contracts,
|
||||||
|
where('groupSlugs', 'array-contains', groupSlug),
|
||||||
|
where('isResolved', '==', false),
|
||||||
|
orderBy('popularityScore', 'desc'),
|
||||||
|
limit(count * 2)
|
||||||
|
)
|
||||||
|
const data = await getValues<Contract>(creatorContractsQuery)
|
||||||
|
const open = data
|
||||||
|
.filter((c) => c.closeTime && c.closeTime > Date.now())
|
||||||
|
.filter((c) => !excluding.includes(c.id))
|
||||||
|
|
||||||
|
return chooseRandomSubset(open, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecommendedContracts = async (
|
||||||
|
contract: Contract,
|
||||||
|
count: number
|
||||||
|
) => {
|
||||||
|
const { creatorId, groupSlugs, id } = contract
|
||||||
|
|
||||||
|
const [userContracts, groupContracts] = await Promise.all([
|
||||||
|
getRandTopCreatorContracts(creatorId, count, [id]),
|
||||||
|
groupSlugs && groupSlugs[0]
|
||||||
|
? getRandTopGroupContracts(groupSlugs[0], count, [id])
|
||||||
|
: [],
|
||||||
|
])
|
||||||
|
|
||||||
|
const combined = uniqBy([...userContracts, ...groupContracts], (c) => c.id)
|
||||||
|
|
||||||
|
return chooseRandomSubset(combined, count)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRecentBetsAndComments(contract: Contract) {
|
export async function getRecentBetsAndComments(contract: Contract) {
|
||||||
const contractDoc = doc(contracts, contract.id)
|
const contractDoc = doc(contracts, contract.id)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { collection, limit, orderBy, query, where } from 'firebase/firestore'
|
import { collection, limit, orderBy, query, where } from 'firebase/firestore'
|
||||||
import { Notification } from 'common/notification'
|
|
||||||
import { db } from 'web/lib/firebase/init'
|
import { db } from 'web/lib/firebase/init'
|
||||||
import { listenForValues } from 'web/lib/firebase/utils'
|
|
||||||
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
||||||
|
|
||||||
export function getNotificationsQuery(
|
export function getNotificationsQuery(
|
||||||
|
@ -23,17 +21,3 @@ export function getNotificationsQuery(
|
||||||
limit(NOTIFICATIONS_PER_PAGE * 10)
|
limit(NOTIFICATIONS_PER_PAGE * 10)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForNotifications(
|
|
||||||
userId: string,
|
|
||||||
setNotifications: (notifs: Notification[]) => void,
|
|
||||||
unseenOnlyOptions?: { unseenOnly: boolean; limit: number }
|
|
||||||
) {
|
|
||||||
return listenForValues<Notification>(
|
|
||||||
getNotificationsQuery(userId, unseenOnlyOptions),
|
|
||||||
(notifs) => {
|
|
||||||
notifs.sort((n1, n2) => n2.createdTime - n1.createdTime)
|
|
||||||
setNotifications(notifs)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
34
web/lib/firebase/posts.ts
Normal file
34
web/lib/firebase/posts.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
deleteDoc,
|
||||||
|
doc,
|
||||||
|
getDocs,
|
||||||
|
query,
|
||||||
|
updateDoc,
|
||||||
|
where,
|
||||||
|
} from 'firebase/firestore'
|
||||||
|
import { Post } from 'common/post'
|
||||||
|
import { coll, getValue } from './utils'
|
||||||
|
|
||||||
|
export const posts = coll<Post>('posts')
|
||||||
|
|
||||||
|
export function postPath(postSlug: string) {
|
||||||
|
return `/post/${postSlug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePost(post: Post, updates: Partial<Post>) {
|
||||||
|
return updateDoc(doc(posts, post.id), updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePost(post: Post) {
|
||||||
|
return deleteDoc(doc(posts, post.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPost(postId: string) {
|
||||||
|
return getValue<Post>(doc(posts, postId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPostBySlug(slug: string) {
|
||||||
|
const q = query(posts, where('slug', '==', slug))
|
||||||
|
const docs = (await getDocs(q)).docs
|
||||||
|
return docs.length === 0 ? null : docs[0].data()
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import {
|
||||||
deleteDoc,
|
deleteDoc,
|
||||||
collectionGroup,
|
collectionGroup,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
|
Query,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { getAuth } from 'firebase/auth'
|
import { getAuth } from 'firebase/auth'
|
||||||
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
||||||
|
@ -253,14 +254,13 @@ export async function unfollow(userId: string, unfollowedUserId: string) {
|
||||||
await deleteDoc(followDoc)
|
await deleteDoc(followDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPortfolioHistory(userId: string) {
|
export function getPortfolioHistoryQuery(userId: string, since: number) {
|
||||||
return getValues<PortfolioMetrics>(
|
return query(
|
||||||
query(
|
collectionGroup(db, 'portfolioHistory'),
|
||||||
collectionGroup(db, 'portfolioHistory'),
|
where('userId', '==', userId),
|
||||||
where('userId', '==', userId),
|
where('timestamp', '>=', since),
|
||||||
orderBy('timestamp', 'asc')
|
orderBy('timestamp', 'asc')
|
||||||
)
|
) as Query<PortfolioMetrics>
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenForFollows(
|
export function listenForFollows(
|
||||||
|
|
27
web/lib/icons/trophy-icon.tsx
Normal file
27
web/lib/icons/trophy-icon.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export default function TrophyIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentcolor"
|
||||||
|
strokeWidth="2"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="m6,5c0,4 1.4,7.8 3.5,8.5l0,2c-1.2,0.7 -1.2,1 -1.6,4l8,0c-0.4,-3 -0.4,-3.3 -1.6,-4l0,-2c2.1,-0.7 3.5,-4.5 3.5,-8.5z"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m6.2,8.3c-2.5,-1.6 -3.5,1 -3,2.5c1,1.7 2.6,2.5 4.5,1.8"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m17.6,8.3c2.5,-1.6 3.5,1 3,2.5c-1,1.7 -2.6,2.5 -4.5,1.8"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -27,14 +27,14 @@
|
||||||
"@nivo/line": "0.74.0",
|
"@nivo/line": "0.74.0",
|
||||||
"@nivo/tooltip": "0.74.0",
|
"@nivo/tooltip": "0.74.0",
|
||||||
"@react-query-firebase/firestore": "0.4.2",
|
"@react-query-firebase/firestore": "0.4.2",
|
||||||
"@tiptap/core": "2.0.0-beta.181",
|
"@tiptap/core": "2.0.0-beta.182",
|
||||||
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
||||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||||
"@tiptap/extension-placeholder": "2.0.0-beta.53",
|
"@tiptap/extension-placeholder": "2.0.0-beta.53",
|
||||||
"@tiptap/react": "2.0.0-beta.114",
|
"@tiptap/react": "2.0.0-beta.114",
|
||||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
"@tiptap/starter-kit": "2.0.0-beta.191",
|
||||||
"algoliasearch": "4.13.0",
|
"algoliasearch": "4.13.0",
|
||||||
"browser-image-compression": "2.0.0",
|
"browser-image-compression": "2.0.0",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||||
|
@ -11,7 +11,7 @@ import { Spacer } from 'web/components/layout/spacer'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
getRandTopCreatorContracts,
|
getRecommendedContracts,
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
@ -40,8 +40,9 @@ import {
|
||||||
ContractLeaderboard,
|
ContractLeaderboard,
|
||||||
ContractTopTrades,
|
ContractTopTrades,
|
||||||
} from 'web/components/contract/contract-leaderboard'
|
} from 'web/components/contract/contract-leaderboard'
|
||||||
import { Subtitle } from 'web/components/subtitle'
|
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { usePrefetch } from 'web/hooks/use-prefetch'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -54,9 +55,7 @@ export async function getStaticPropz(props: {
|
||||||
const [bets, comments, recommendedContracts] = await Promise.all([
|
const [bets, comments, recommendedContracts] = await Promise.all([
|
||||||
contractId ? listAllBets(contractId) : [],
|
contractId ? listAllBets(contractId) : [],
|
||||||
contractId ? listAllComments(contractId) : [],
|
contractId ? listAllComments(contractId) : [],
|
||||||
contract
|
contract ? getRecommendedContracts(contract, 6) : [],
|
||||||
? getRandTopCreatorContracts(contract.creatorId, 4, [contract?.id])
|
|
||||||
: [],
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -108,7 +107,9 @@ export default function ContractPage(props: {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ContractPageContent {...{ ...props, contract, user }} />
|
return (
|
||||||
|
<ContractPageContent key={contract.id} {...{ ...props, contract, user }} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContractPageSidebar(props: {
|
export function ContractPageSidebar(props: {
|
||||||
|
@ -154,9 +155,10 @@ export function ContractPageContent(
|
||||||
user?: User | null
|
user?: User | null
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { backToHome, comments, user, recommendedContracts } = props
|
const { backToHome, comments, user } = props
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.contract) ?? props.contract
|
const contract = useContractWithPreload(props.contract) ?? props.contract
|
||||||
|
usePrefetch(user?.id)
|
||||||
|
|
||||||
useTracking('view market', {
|
useTracking('view market', {
|
||||||
slug: contract.slug,
|
slug: contract.slug,
|
||||||
|
@ -165,6 +167,10 @@ export function ContractPageContent(
|
||||||
})
|
})
|
||||||
|
|
||||||
const bets = useBets(contract.id) ?? props.bets
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
|
const nonChallengeBets = useMemo(
|
||||||
|
() => bets.filter((b) => !b.challengeSlug),
|
||||||
|
[bets]
|
||||||
|
)
|
||||||
|
|
||||||
// Sort for now to see if bug is fixed.
|
// Sort for now to see if bug is fixed.
|
||||||
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||||
|
@ -182,6 +188,16 @@ export function ContractPageContent(
|
||||||
setShowConfetti(shouldSeeConfetti)
|
setShowConfetti(shouldSeeConfetti)
|
||||||
}, [contract, user])
|
}, [contract, user])
|
||||||
|
|
||||||
|
const [recommendedContracts, setRecommendedMarkets] = useState(
|
||||||
|
props.recommendedContracts
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (contract && recommendedContracts.length === 0) {
|
||||||
|
getRecommendedContracts(contract, 6).then(setRecommendedMarkets)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [contract.id, recommendedContracts])
|
||||||
|
|
||||||
const { isResolved, question, outcomeType } = contract
|
const { isResolved, question, outcomeType } = contract
|
||||||
|
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
|
@ -220,10 +236,7 @@ export function ContractPageContent(
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContractOverview
|
<ContractOverview contract={contract} bets={nonChallengeBets} />
|
||||||
contract={contract}
|
|
||||||
bets={bets.filter((b) => !b.challengeSlug)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{outcomeType === 'NUMERIC' && (
|
{outcomeType === 'NUMERIC' && (
|
||||||
<AlertBox
|
<AlertBox
|
||||||
|
@ -267,14 +280,17 @@ export function ContractPageContent(
|
||||||
tips={tips}
|
tips={tips}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{recommendedContracts?.length > 0 && (
|
|
||||||
<Col className="mx-2 gap-2 sm:mx-0">
|
|
||||||
<Subtitle text="Recommended" />
|
|
||||||
<ContractsGrid contracts={recommendedContracts} />
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{recommendedContracts.length > 0 && (
|
||||||
|
<Col className="mt-2 gap-2 px-2 sm:px-0">
|
||||||
|
<Title className="text-gray-700" text="Recommended" />
|
||||||
|
<ContractsGrid
|
||||||
|
contracts={recommendedContracts}
|
||||||
|
trackingPostfix=" recommended"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
93
web/pages/create-post.tsx
Normal file
93
web/pages/create-post.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
|
import { Page } from 'web/components/page'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
|
import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
|
import { createPost } from 'web/lib/firebase/api'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import { MAX_POST_TITLE_LENGTH } from 'common/post'
|
||||||
|
import { postPath } from 'web/lib/firebase/posts'
|
||||||
|
|
||||||
|
export default function CreatePost() {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const { editor, upload } = useTextEditor({
|
||||||
|
disabled: isSubmitting,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValid = editor && title.length > 0 && editor.isEmpty === false
|
||||||
|
|
||||||
|
async function savePost(title: string) {
|
||||||
|
if (!editor) return
|
||||||
|
const newPost = {
|
||||||
|
title: title,
|
||||||
|
content: editor.getJSON(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createPost(newPost).catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
setError('There was an error creating the post, please try again')
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
if (result.post) {
|
||||||
|
await Router.push(postPath(result.post.slug))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="mx-auto w-full max-w-2xl">
|
||||||
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
|
<Title className="!mt-0" text="Create a post" />
|
||||||
|
<form>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Title<span className={'text-red-700'}> *</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. Elon Mania Post"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
|
maxLength={MAX_POST_TITLE_LENGTH}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
<Spacer h={6} />
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Content<span className={'text-red-700'}> *</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor editor={editor} upload={upload} />
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
'btn btn-primary normal-case',
|
||||||
|
isSubmitting && 'loading disabled'
|
||||||
|
)}
|
||||||
|
disabled={isSubmitting || !isValid || upload.isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
await savePost(title)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Creating...' : 'Create a post'}
|
||||||
|
</button>
|
||||||
|
{error !== '' && <div className="text-red-700">{error}</div>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers'
|
import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers'
|
||||||
|
import { MINUTE_MS } from 'common/util/time'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
|
||||||
return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } }
|
return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } }
|
||||||
|
@ -66,6 +67,18 @@ export default function Create(props: { auth: { user: User } }) {
|
||||||
|
|
||||||
if (!router.isReady) return <div />
|
if (!router.isReady) return <div />
|
||||||
|
|
||||||
|
if (user.isBannedFromPosting)
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="mx-auto w-full max-w-2xl">
|
||||||
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
|
<Title className="!mt-0" text="Create a market" />
|
||||||
|
<p>Sorry, you are currently banned from creating a market.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -209,7 +222,9 @@ export function NewContract(props: {
|
||||||
max: MAX_DESCRIPTION_LENGTH,
|
max: MAX_DESCRIPTION_LENGTH,
|
||||||
placeholder: descriptionPlaceholder,
|
placeholder: descriptionPlaceholder,
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
defaultValue: JSON.parse(params?.description ?? '{}'),
|
defaultValue: params?.description
|
||||||
|
? JSON.parse(params.description)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEditorFilled = editor != null && !editor.isEmpty
|
const isEditorFilled = editor != null && !editor.isEmpty
|
||||||
|
@ -427,7 +442,7 @@ export function NewContract(props: {
|
||||||
className="input input-bordered mt-4"
|
className="input input-bordered mt-4"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setCloseDate(e.target.value)}
|
onChange={(e) => setCloseDate(e.target.value)}
|
||||||
min={Date.now()}
|
min={Math.round(Date.now() / MINUTE_MS) * MINUTE_MS}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={closeDate}
|
value={closeDate}
|
||||||
/>
|
/>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user