Compare commits
65 Commits
main
...
salemcente
Author | SHA1 | Date | |
---|---|---|---|
|
fe77615111 | ||
|
66b63881a1 | ||
|
1e2593894c | ||
|
3b390f758f | ||
|
b8b2778b69 | ||
|
d081f5215d | ||
|
b56cfd510f | ||
|
f407737f0c | ||
|
4ab86963e8 | ||
|
1dc94114e3 | ||
|
f500064e4e | ||
|
e4a2f8acbb | ||
|
a266de380f | ||
|
f32d207178 | ||
|
919af972c8 | ||
|
8f157946fa | ||
|
f6bb1b9e38 | ||
|
9d51f7a662 | ||
|
960b118e90 | ||
|
d70838c09b | ||
|
adbf06e889 | ||
|
dc44df3968 | ||
|
3878b08d77 | ||
|
9cc1a5199b | ||
|
272d5ec2e2 | ||
|
b6b670214d | ||
|
a9120312af | ||
|
bf13e2fc12 | ||
|
367046a6e0 | ||
|
c921590643 | ||
|
0066589569 | ||
|
6f196c7518 | ||
|
166e228032 | ||
|
76923c773e | ||
|
90c707516b | ||
|
ffa74c8e10 | ||
|
13a2877e02 | ||
|
96abf43977 | ||
|
24655c0d7f | ||
|
1aed9bb364 | ||
|
b8cdb6104d | ||
|
0f28fe3302 | ||
|
f61c2c2cc0 | ||
|
6be321fb88 | ||
|
bd5d3d2afc | ||
|
b24035f0a8 | ||
|
09d95f2b2b | ||
|
f62b1037ff | ||
|
7673b32a0c | ||
|
f6c1f46229 | ||
|
5bfd7d80b0 | ||
|
efcfa10323 | ||
|
02aa13f7d1 | ||
|
c15292e2cb | ||
|
bfc1f38477 | ||
|
240355f717 | ||
|
6a5fe931dd | ||
|
a6f4a5fc22 | ||
|
d3b78eeb42 | ||
|
b7b6e10968 | ||
|
471e55665d | ||
|
c63070744d | ||
|
d9d2dee576 | ||
|
133ef04826 | ||
|
0f77b08319 |
|
@ -1,3 +1,5 @@
|
||||||
|
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
|
||||||
|
|
||||||
export type Challenge = {
|
export type Challenge = {
|
||||||
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
|
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
|
||||||
// Also functions as the unique id for the link.
|
// Also functions as the unique id for the link.
|
||||||
|
@ -60,4 +62,4 @@ export type Acceptance = {
|
||||||
createdTime: number
|
createdTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHALLENGES_ENABLED = true
|
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { escapeRegExp } from 'lodash'
|
||||||
import { DEV_CONFIG } from './dev'
|
import { DEV_CONFIG } from './dev'
|
||||||
import { EnvConfig, PROD_CONFIG } from './prod'
|
import { EnvConfig, PROD_CONFIG } from './prod'
|
||||||
import { THEOREMONE_CONFIG } from './theoremone'
|
import { THEOREMONE_CONFIG } from './theoremone'
|
||||||
|
import { SALEM_CENTER_CONFIG } from './salemcenter'
|
||||||
|
|
||||||
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ const CONFIGS: { [env: string]: EnvConfig } = {
|
||||||
PROD: PROD_CONFIG,
|
PROD: PROD_CONFIG,
|
||||||
DEV: DEV_CONFIG,
|
DEV: DEV_CONFIG,
|
||||||
THEOREMONE: THEOREMONE_CONFIG,
|
THEOREMONE: THEOREMONE_CONFIG,
|
||||||
|
SALEM_CENTER: SALEM_CENTER_CONFIG,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ENV_CONFIG = CONFIGS[ENV]
|
export const ENV_CONFIG = CONFIGS[ENV]
|
||||||
|
|
|
@ -18,6 +18,7 @@ export type EnvConfig = {
|
||||||
faviconPath?: string // Should be a file in /public
|
faviconPath?: string // Should be a file in /public
|
||||||
navbarLogoPath?: string
|
navbarLogoPath?: string
|
||||||
newQuestionPlaceholders: string[]
|
newQuestionPlaceholders: string[]
|
||||||
|
whitelistCreators?: string[]
|
||||||
|
|
||||||
// Currency controls
|
// Currency controls
|
||||||
fixedAnte?: number
|
fixedAnte?: number
|
||||||
|
|
25
common/envs/salemcenter.ts
Normal file
25
common/envs/salemcenter.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { EnvConfig, PROD_CONFIG } from './prod'
|
||||||
|
|
||||||
|
export const SALEM_CENTER_CONFIG: EnvConfig = {
|
||||||
|
domain: 'salemcenter.manifold.markets',
|
||||||
|
amplitudeApiKey: '3ffa2921424f251908926086f37bc447',
|
||||||
|
firebaseConfig: {
|
||||||
|
apiKey: 'AIzaSyBxisXMHPJDtM7ZseaOOlLAM_T7QHP_QvA',
|
||||||
|
authDomain: 'salem-center-manifold.firebaseapp.com',
|
||||||
|
projectId: 'salem-center-manifold',
|
||||||
|
region: 'us-central1',
|
||||||
|
storageBucket: 'salem-center-manifold.appspot.com',
|
||||||
|
messagingSenderId: '522400938664',
|
||||||
|
appId: '1:522400938664:web:300eaedb8446818d61a09d',
|
||||||
|
measurementId: 'G-Y3EZ1WNT6E',
|
||||||
|
},
|
||||||
|
cloudRunId: 'fm35sk365q', // TODO: fill in real ID for T1
|
||||||
|
cloudRunRegion: 'uc',
|
||||||
|
adminEmails: [...PROD_CONFIG.adminEmails, 'richardh0828@gmail.com'],
|
||||||
|
moneyMoniker: 'S$',
|
||||||
|
visibility: 'PRIVATE',
|
||||||
|
faviconPath: '/salem-center/logo.ico',
|
||||||
|
navbarLogoPath: '/salem-center/salem-center-logo.svg',
|
||||||
|
newQuestionPlaceholders: [],
|
||||||
|
whitelistCreators: ['RichardHanania', 'SalemCenter'],
|
||||||
|
}
|
|
@ -528,6 +528,10 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
|
||||||
"contractId":"{...}"}'
|
"contractId":"{...}"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `POST /v0/bet/cancel/[id]`
|
||||||
|
|
||||||
|
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
|
||||||
|
|
||||||
### `POST /v0/market`
|
### `POST /v0/market`
|
||||||
|
|
||||||
Creates a new market on behalf of the authorized user.
|
Creates a new market on behalf of the authorized user.
|
||||||
|
|
|
@ -22,6 +22,20 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"collectionGroup": "bets",
|
||||||
|
"queryScope": "COLLECTION_GROUP",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "isFilled",
|
||||||
|
"order": "ASCENDING"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"order": "ASCENDING"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collectionGroup": "bets",
|
"collectionGroup": "bets",
|
||||||
"queryScope": "COLLECTION_GROUP",
|
"queryScope": "COLLECTION_GROUP",
|
||||||
|
|
|
@ -10,7 +10,8 @@ service cloud.firestore {
|
||||||
'akrolsmir@gmail.com',
|
'akrolsmir@gmail.com',
|
||||||
'jahooma@gmail.com',
|
'jahooma@gmail.com',
|
||||||
'taowell@gmail.com',
|
'taowell@gmail.com',
|
||||||
'manticmarkets@gmail.com'
|
'manticmarkets@gmail.com',
|
||||||
|
'richardh0828@gmail.com',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# This sets which EnvConfig is deployed to Firebase Cloud Functions
|
# This sets which EnvConfig is deployed to Firebase Cloud Functions
|
||||||
|
|
||||||
NEXT_PUBLIC_FIREBASE_ENV=PROD
|
NEXT_PUBLIC_FIREBASE_ENV=SALEM_CENTER
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
cleanDisplayName,
|
cleanDisplayName,
|
||||||
cleanUsername,
|
cleanUsername,
|
||||||
} from '../../common/util/clean-username'
|
} from '../../common/util/clean-username'
|
||||||
import { sendWelcomeEmail } from './emails'
|
|
||||||
import { isWhitelisted } from '../../common/envs/constants'
|
import { isWhitelisted } from '../../common/envs/constants'
|
||||||
import {
|
import {
|
||||||
CATEGORIES_GROUP_SLUG_POSTFIX,
|
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||||
|
@ -94,8 +93,8 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||||
|
|
||||||
await addUserToDefaultGroups(user)
|
// await addUserToDefaultGroups(user)
|
||||||
await sendWelcomeEmail(user, privateUser)
|
// await sendWelcomeEmail(user, privateUser)
|
||||||
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -121,7 +120,7 @@ export const numberUsersWithIp = async (ipAddress: string) => {
|
||||||
return snap.docs.length
|
return snap.docs.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const addUserToDefaultGroups = async (user: User) => {
|
const _addUserToDefaultGroups = async (user: User) => {
|
||||||
for (const category of Object.values(DEFAULT_CATEGORIES)) {
|
for (const category of Object.values(DEFAULT_CATEGORIES)) {
|
||||||
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
|
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
|
||||||
const groups = await getValues<Group>(
|
const groups = await getValues<Group>(
|
||||||
|
|
|
@ -12,13 +12,12 @@ const firestore = admin.firestore()
|
||||||
|
|
||||||
export const onUpdateUser = functions.firestore
|
export const onUpdateUser = functions.firestore
|
||||||
.document('users/{userId}')
|
.document('users/{userId}')
|
||||||
.onUpdate(async (change, context) => {
|
.onUpdate(async (change, _context) => {
|
||||||
const prevUser = change.before.data() as User
|
const prevUser = change.before.data() as User
|
||||||
const user = change.after.data() as User
|
const user = change.after.data() as User
|
||||||
const { eventId } = context
|
|
||||||
|
|
||||||
if (prevUser.referredByUserId !== user.referredByUserId) {
|
if (prevUser.referredByUserId !== user.referredByUserId) {
|
||||||
await handleUserUpdatedReferral(user, eventId)
|
// await handleUserUpdatedReferral(user, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.balance <= 0) {
|
if (user.balance <= 0) {
|
||||||
|
@ -26,7 +25,7 @@ export const onUpdateUser = functions.firestore
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleUserUpdatedReferral(user: User, eventId: string) {
|
async function _handleUserUpdatedReferral(user: User, eventId: string) {
|
||||||
// Only create a referral txn if the user has a referredByUserId
|
// Only create a referral txn if the user has a referredByUserId
|
||||||
if (!user.referredByUserId) {
|
if (!user.referredByUserId) {
|
||||||
console.log(`Not set: referredByUserId ${user.referredByUserId}`)
|
console.log(`Not set: referredByUserId ${user.referredByUserId}`)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge } from 'common/challenge'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export type OgCardProps = {
|
export type OgCardProps = {
|
||||||
question: string
|
question: string
|
||||||
|
@ -88,7 +89,7 @@ export function SEO(props: {
|
||||||
{url && (
|
{url && (
|
||||||
<meta
|
<meta
|
||||||
property="og:url"
|
property="og:url"
|
||||||
content={'https://manifold.markets' + url}
|
content={'https://' + ENV_CONFIG.domain + url}
|
||||||
key="url"
|
key="url"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { SiteLink } from './site-link'
|
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export function AmountInput(props: {
|
export function AmountInput(props: {
|
||||||
|
@ -61,16 +60,7 @@ export function AmountInput(props: {
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||||
{error === 'Insufficient balance' ? (
|
{error === 'Insufficient balance' ? <>Not enough funds.</> : error}
|
||||||
<>
|
|
||||||
Not enough funds.
|
|
||||||
<span className="ml-1 text-indigo-500">
|
|
||||||
<SiteLink href="/add-funds">Buy more?</SiteLink>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
error
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import clsx from 'clsx'
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Bet } from 'web/lib/firebase/bets'
|
import { Bet } from 'web/lib/firebase/bets'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { getFirstDayProfit, User } from 'web/lib/firebase/users'
|
||||||
import {
|
import {
|
||||||
formatLargeNumber,
|
formatLargeNumber,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
@ -73,6 +73,7 @@ export function BetsList(props: {
|
||||||
|
|
||||||
const [sort, setSort] = useState<BetSort>('newest')
|
const [sort, setSort] = useState<BetSort>('newest')
|
||||||
const [filter, setFilter] = useState<BetFilter>('open')
|
const [filter, setFilter] = useState<BetFilter>('open')
|
||||||
|
const [firstDayProfit, setFirstDayProfit] = useState(0)
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const start = page * CONTRACTS_PER_PAGE
|
const start = page * CONTRACTS_PER_PAGE
|
||||||
const end = start + CONTRACTS_PER_PAGE
|
const end = start + CONTRACTS_PER_PAGE
|
||||||
|
@ -84,6 +85,12 @@ export function BetsList(props: {
|
||||||
}
|
}
|
||||||
}, [signedInUser, bets, contractsById, getTime])
|
}, [signedInUser, bets, contractsById, getTime])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (signedInUser && signedInUser.id === user.id) {
|
||||||
|
getFirstDayProfit(signedInUser.id).then(setFirstDayProfit)
|
||||||
|
}
|
||||||
|
}, [signedInUser, user])
|
||||||
|
|
||||||
if (!bets || !contractsById) {
|
if (!bets || !contractsById) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
@ -157,30 +164,33 @@ export function BetsList(props: {
|
||||||
(c) => contractsMetrics[c.id].netPayout
|
(c) => contractsMetrics[c.id].netPayout
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalPnl = user.profitCached.allTime
|
const totalPnl = (signedInUser?.profitCached.allTime ?? 0) - firstDayProfit
|
||||||
const totalProfitPercent = (totalPnl / user.totalDeposits) * 100
|
const totalProfitPercent =
|
||||||
|
(totalPnl / (signedInUser?.totalDeposits ?? 1000)) * 100
|
||||||
const investedProfitPercent =
|
const investedProfitPercent =
|
||||||
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
|
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="mt-6">
|
<Col className="mt-6">
|
||||||
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
|
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
|
||||||
<Row className="gap-8">
|
{user.id === signedInUser?.id && (
|
||||||
<Col>
|
<Row className="gap-8">
|
||||||
<div className="text-sm text-gray-500">Investment value</div>
|
<Col>
|
||||||
<div className="text-lg">
|
<div className="text-sm text-gray-500">Investment value</div>
|
||||||
{formatMoney(currentNetInvestment)}{' '}
|
<div className="text-lg">
|
||||||
<ProfitBadge profitPercent={investedProfitPercent} />
|
{formatMoney(currentNetInvestment)}{' '}
|
||||||
</div>
|
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||||
</Col>
|
</div>
|
||||||
<Col>
|
</Col>
|
||||||
<div className="text-sm text-gray-500">Total profit</div>
|
<Col>
|
||||||
<div className="text-lg">
|
<div className="text-sm text-gray-500">Total profit</div>
|
||||||
{formatMoney(totalPnl)}{' '}
|
<div className="text-lg">
|
||||||
<ProfitBadge profitPercent={totalProfitPercent} />
|
{formatMoney(totalPnl)}{' '}
|
||||||
</div>
|
<ProfitBadge profitPercent={totalProfitPercent} />
|
||||||
</Col>
|
</div>
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row className="gap-8">
|
<Row className="gap-8">
|
||||||
<select
|
<select
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { Col } from '../layout/col'
|
||||||
import { Modal } from '../layout/modal'
|
import { Modal } from '../layout/modal'
|
||||||
import { Title } from '../title'
|
import { Title } from '../title'
|
||||||
import { InfoTooltip } from '../info-tooltip'
|
import { InfoTooltip } from '../info-tooltip'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
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'
|
||||||
|
@ -20,6 +22,8 @@ export const contractDetailsButtonClassName =
|
||||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { contract, bets } = props
|
const { contract, bets } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
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 z')
|
||||||
|
@ -124,9 +128,11 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{contract.mechanism === 'cpmm-1' && !contract.resolution && (
|
{contract.mechanism === 'cpmm-1' &&
|
||||||
<LiquidityPanel contract={contract} />
|
!contract.resolution &&
|
||||||
)}
|
ENV_CONFIG.whitelistCreators?.includes(user?.username ?? '') && (
|
||||||
|
<LiquidityPanel contract={contract} />
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { PaperAirplaneIcon } from '@heroicons/react/outline'
|
import { PaperAirplaneIcon } from '@heroicons/react/outline'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
import { Tipper } from '../tipper'
|
|
||||||
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
|
@ -189,7 +188,6 @@ export function FeedComment(props: {
|
||||||
const {
|
const {
|
||||||
contract,
|
contract,
|
||||||
comment,
|
comment,
|
||||||
tips,
|
|
||||||
betsBySameUser,
|
betsBySameUser,
|
||||||
probAtCreatedTime,
|
probAtCreatedTime,
|
||||||
truncate,
|
truncate,
|
||||||
|
@ -282,7 +280,6 @@ export function FeedComment(props: {
|
||||||
shouldTruncate={truncate}
|
shouldTruncate={truncate}
|
||||||
/>
|
/>
|
||||||
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
||||||
<Tipper comment={comment} tips={tips ?? {}} />
|
|
||||||
{onReplyClick && (
|
{onReplyClick && (
|
||||||
<button
|
<button
|
||||||
className="font-bold hover:underline"
|
className="font-bold hover:underline"
|
||||||
|
|
|
@ -357,7 +357,6 @@ const GroupMessage = memo(function GroupMessage_(props: {
|
||||||
{formatMoney(sum(Object.values(tips)))}
|
{formatMoney(sum(Object.values(tips)))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isCreatorsComment && <Tipper comment={comment} tips={tips} />}
|
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import { SparklesIcon } from '@heroicons/react/solid'
|
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
|
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import { ContractsGrid } from './contract/contracts-list'
|
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
import { withTracking } from 'web/lib/service/analytics'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
|
import { SiteLink } from './site-link'
|
||||||
|
|
||||||
export function LandingPagePanel(props: { hotContracts: Contract[] }) {
|
export function LandingPagePanel() {
|
||||||
const { hotContracts } = props
|
|
||||||
|
|
||||||
useTracking('view landing page')
|
useTracking('view landing page')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -27,23 +20,28 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
|
||||||
<div className="m-4 max-w-[550px] self-center">
|
<div className="m-4 max-w-[550px] self-center">
|
||||||
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
|
||||||
<div className="font-semibold sm:mb-2">
|
<div className="font-semibold sm:mb-2">
|
||||||
Predict{' '}
|
CSPI/Salem{' '}
|
||||||
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
|
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
|
||||||
anything!
|
Tournament
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<div className="mb-4 px-2 ">
|
<div className="mb-4 px-2 ">
|
||||||
Create a play-money prediction market on any topic you care about
|
Predict the future and win!
|
||||||
and bet with your friends on what will happen!
|
|
||||||
<br />
|
<br />
|
||||||
{/* <br />
|
<br />
|
||||||
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
|
Manifold Markets is partnering with CSPI and the Salem Center of the
|
||||||
<SiteLink className="font-semibold" href="/charity">
|
University of Texas at Austin to bring a{' '}
|
||||||
favorite charity.
|
<SiteLink
|
||||||
</SiteLink>
|
className="underline"
|
||||||
<br /> */}
|
href={
|
||||||
|
'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
forecasting tournament
|
||||||
|
</SiteLink>
|
||||||
|
.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
@ -54,16 +52,6 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
|
||||||
Get started
|
Get started
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
|
|
||||||
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
|
|
||||||
Trending markets
|
|
||||||
</Row>
|
|
||||||
<ContractsGrid
|
|
||||||
contracts={hotContracts?.slice(0, 10) || []}
|
|
||||||
loadMore={() => {}}
|
|
||||||
hasMore={false}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,16 +17,14 @@ export function ManifoldLogo(props: {
|
||||||
return (
|
return (
|
||||||
<Link href={user ? '/home' : '/'}>
|
<Link href={user ? '/home' : '/'}>
|
||||||
<a className={clsx('group flex flex-shrink-0 flex-row gap-4', className)}>
|
<a className={clsx('group flex flex-shrink-0 flex-row gap-4', className)}>
|
||||||
<img
|
|
||||||
className="transition-all group-hover:rotate-12"
|
|
||||||
src={darkBackground ? '/logo-white.svg' : '/logo.svg'}
|
|
||||||
width={45}
|
|
||||||
height={45}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!hideText &&
|
{!hideText &&
|
||||||
(ENV_CONFIG.navbarLogoPath ? (
|
(ENV_CONFIG.navbarLogoPath ? (
|
||||||
<img src={ENV_CONFIG.navbarLogoPath} width={245} height={45} />
|
<img
|
||||||
|
src={ENV_CONFIG.navbarLogoPath}
|
||||||
|
width={245}
|
||||||
|
height={45}
|
||||||
|
className="rounded-full bg-gray-800 px-6 py-2"
|
||||||
|
/>
|
||||||
) : twoLine ? (
|
) : twoLine ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
|
@ -5,30 +5,24 @@ import {
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
CashIcon,
|
CashIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
UserGroupIcon,
|
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
ChatIcon,
|
ChatIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Router, { useRouter } from 'next/router'
|
import Router, { useRouter } from 'next/router'
|
||||||
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { firebaseLogout, User } from 'web/lib/firebase/users'
|
import { firebaseLogout, User } from 'web/lib/firebase/users'
|
||||||
import { ManifoldLogo } from './manifold-logo'
|
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, { useMemo, useState } from 'react'
|
import { ENV_CONFIG, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import React from 'react'
|
||||||
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, GROUP_CHAT_SLUG } from 'common/group'
|
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
|
|
||||||
import { PrivateUser } from 'common/user'
|
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
|
@ -47,17 +41,31 @@ function getNavigation() {
|
||||||
icon: NotificationsIcon,
|
icon: NotificationsIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
{ name: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
|
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? [
|
||||||
|
{
|
||||||
|
name: 'Rules',
|
||||||
|
href: 'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting',
|
||||||
|
icon: BookOpenIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMoreNavigation(user?: User | null) {
|
function getMoreNavigation(user?: User | null) {
|
||||||
if (IS_PRIVATE_MANIFOLD) {
|
if (IS_PRIVATE_MANIFOLD) {
|
||||||
return [{ name: 'Leaderboards', href: '/leaderboards' }]
|
return [
|
||||||
|
{ name: 'Discord', href: 'https://discord.gg/ZtT7PxapSS' },
|
||||||
|
{ name: 'Manifold Markets', href: 'https://manifold.markets' },
|
||||||
|
{
|
||||||
|
name: 'Sign out',
|
||||||
|
href: '#',
|
||||||
|
onClick: withTracking(firebaseLogout, 'sign out'),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -110,34 +118,65 @@ function getMoreNavigation(user?: User | null) {
|
||||||
const signedOutNavigation = [
|
const signedOutNavigation = [
|
||||||
{ name: 'Home', href: '/home', icon: HomeIcon },
|
{ name: 'Home', href: '/home', icon: HomeIcon },
|
||||||
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
{ name: 'Explore', href: '/markets', icon: SearchIcon },
|
||||||
{
|
|
||||||
name: 'About',
|
|
||||||
href: 'https://docs.manifold.markets/$how-to',
|
|
||||||
icon: BookOpenIcon,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const signedOutMobileNavigation = [
|
const signedOutMobileNavigation = IS_PRIVATE_MANIFOLD
|
||||||
{
|
? [
|
||||||
name: 'About',
|
{ name: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
href: 'https://docs.manifold.markets/$how-to',
|
{
|
||||||
icon: BookOpenIcon,
|
name: 'Rules',
|
||||||
},
|
href: 'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting',
|
||||||
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
icon: BookOpenIcon,
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
},
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon },
|
{
|
||||||
]
|
name: 'Discord',
|
||||||
|
href: 'https://discord.gg/ZtT7PxapSS',
|
||||||
|
icon: ChatIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manifold Markets',
|
||||||
|
href: 'https://manifold.markets',
|
||||||
|
icon: ExternalLinkIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ name: 'Charity', href: '/charity', icon: HeartIcon },
|
||||||
|
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
|
{
|
||||||
|
name: 'Discord',
|
||||||
|
href: 'https://discord.gg/eHQBNBqXuh',
|
||||||
|
icon: ChatIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const signedInMobileNavigation = [
|
const signedInMobileNavigation = [
|
||||||
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
|
{ name: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
|
||||||
...(IS_PRIVATE_MANIFOLD
|
...(IS_PRIVATE_MANIFOLD
|
||||||
? []
|
? [
|
||||||
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
|
{
|
||||||
{
|
name: 'Rules',
|
||||||
name: 'About',
|
href: 'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting',
|
||||||
href: 'https://docs.manifold.markets/$how-to',
|
icon: BookOpenIcon,
|
||||||
icon: BookOpenIcon,
|
},
|
||||||
},
|
{
|
||||||
|
name: 'Discord',
|
||||||
|
href: 'https://discord.gg/ZtT7PxapSS',
|
||||||
|
icon: ChatIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manifold Markets',
|
||||||
|
href: 'https://manifold.markets',
|
||||||
|
icon: ExternalLinkIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||||
|
{
|
||||||
|
name: 'About',
|
||||||
|
href: 'https://docs.manifold.markets/$how-to',
|
||||||
|
icon: BookOpenIcon,
|
||||||
|
},
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
function getMoreMobileNav() {
|
function getMoreMobileNav() {
|
||||||
|
@ -232,7 +271,6 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
const currentPage = router.pathname
|
const currentPage = router.pathname
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const privateUser = usePrivateUser(user?.id)
|
|
||||||
// usePing(user?.id)
|
// usePing(user?.id)
|
||||||
|
|
||||||
const navigationOptions = !user ? signedOutNavigation : getNavigation()
|
const navigationOptions = !user ? signedOutNavigation : getNavigation()
|
||||||
|
@ -240,22 +278,13 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
? signedOutMobileNavigation
|
? signedOutMobileNavigation
|
||||||
: signedInMobileNavigation
|
: signedInMobileNavigation
|
||||||
|
|
||||||
const memberItems = (
|
|
||||||
useMemberGroups(
|
|
||||||
user?.id,
|
|
||||||
{ withChatEnabled: true },
|
|
||||||
{ by: 'mostRecentChatActivityTime' }
|
|
||||||
) ?? []
|
|
||||||
).map((group: Group) => ({
|
|
||||||
name: group.name,
|
|
||||||
href: `${groupPath(group.slug)}`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="Sidebar" className={className}>
|
<nav aria-label="Sidebar" className={className}>
|
||||||
<ManifoldLogo className="py-6" twoLine />
|
<ManifoldLogo className="py-6" twoLine />
|
||||||
|
|
||||||
<CreateQuestionButton user={user} />
|
{ENV_CONFIG.whitelistCreators?.includes(user?.username ?? '') && (
|
||||||
|
<CreateQuestionButton user={user} />
|
||||||
|
)}
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
{user && (
|
{user && (
|
||||||
<div className="w-full" style={{ minHeight: 80 }}>
|
<div className="w-full" style={{ minHeight: 80 }}>
|
||||||
|
@ -275,17 +304,6 @@ 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" />
|
|
||||||
)}
|
|
||||||
{privateUser && (
|
|
||||||
<GroupsList
|
|
||||||
currentPage={router.asPath}
|
|
||||||
memberItems={memberItems}
|
|
||||||
privateUser={privateUser}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop navigation */}
|
{/* Desktop navigation */}
|
||||||
|
@ -293,85 +311,13 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
{navigationOptions.map((item) => (
|
{navigationOptions.map((item) => (
|
||||||
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
|
||||||
))}
|
))}
|
||||||
<MenuButton
|
{user && (
|
||||||
menuItems={getMoreNavigation(user)}
|
<MenuButton
|
||||||
buttonContent={<MoreButton />}
|
menuItems={getMoreNavigation(user)}
|
||||||
/>
|
buttonContent={<MoreButton />}
|
||||||
|
|
||||||
{/* Spacer if there are any groups */}
|
|
||||||
{memberItems.length > 0 && <hr className="!my-4 border-gray-300" />}
|
|
||||||
{privateUser && (
|
|
||||||
<GroupsList
|
|
||||||
currentPage={router.asPath}
|
|
||||||
memberItems={memberItems}
|
|
||||||
privateUser={privateUser}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupsList(props: {
|
|
||||||
currentPage: string
|
|
||||||
memberItems: Item[]
|
|
||||||
privateUser: PrivateUser
|
|
||||||
}) {
|
|
||||||
const { currentPage, memberItems, privateUser } = props
|
|
||||||
const preferredNotifications = useUnseenPreferredNotifications(
|
|
||||||
privateUser,
|
|
||||||
{
|
|
||||||
customHref: '/group/',
|
|
||||||
},
|
|
||||||
memberItems.length > 0 ? memberItems.length : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const { height } = useWindowSize()
|
|
||||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null)
|
|
||||||
const remainingHeight =
|
|
||||||
(height ?? window.innerHeight) - (containerRef?.offsetTop ?? 0)
|
|
||||||
|
|
||||||
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 +
|
|
||||||
(notifIsForThisItem(item.href) ? '/' + GROUP_CHAT_SLUG : '')
|
|
||||||
}
|
|
||||||
key={item.name}
|
|
||||||
onClick={trackCallback('sidebar: ' + 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',
|
|
||||||
notifIsForThisItem(item.href) && 'font-bold'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { InfoBox } from './info-box'
|
import { InfoBox } from './info-box'
|
||||||
|
|
||||||
export const PlayMoneyDisclaimer = () => (
|
export const PlayMoneyDisclaimer = () => (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
title="Play-money betting"
|
title="Play-money betting"
|
||||||
className="mt-4 max-w-md"
|
className="mt-4 max-w-md"
|
||||||
text="Mana (M$) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!"
|
text={`Mana (${ENV_CONFIG.moneyMoniker}) is the play-money used by our platform to keep track of your bets. It's completely free for you and your friends to get started!`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,8 +39,6 @@ import { PortfolioValueSection } from './portfolio/portfolio-value-section'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
import { useUserBets } from 'web/hooks/use-user-bets'
|
import { useUserBets } from 'web/hooks/use-user-bets'
|
||||||
import { ReferralsButton } from 'web/components/referrals-button'
|
import { ReferralsButton } from 'web/components/referrals-button'
|
||||||
import { formatMoney } from 'common/util/format'
|
|
||||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export function UserLink(props: {
|
export function UserLink(props: {
|
||||||
|
@ -122,7 +120,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
|
|
||||||
const yourFollows = useFollows(currentUser?.id)
|
const yourFollows = useFollows(currentUser?.id)
|
||||||
const isFollowing = yourFollows?.includes(user.id)
|
const isFollowing = yourFollows?.includes(user.id)
|
||||||
const profit = user.profitCached.allTime
|
|
||||||
|
|
||||||
const onFollow = () => {
|
const onFollow = () => {
|
||||||
if (!currentUser) return
|
if (!currentUser) return
|
||||||
|
@ -187,17 +184,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
<Col className="mx-4 -mt-6">
|
<Col className="mx-4 -mt-6">
|
||||||
<span className="text-2xl font-bold">{user.name}</span>
|
<span className="text-2xl font-bold">{user.name}</span>
|
||||||
<span className="text-gray-500">@{user.username}</span>
|
<span className="text-gray-500">@{user.username}</span>
|
||||||
<span className="text-gray-500">
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'text-md',
|
|
||||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatMoney(profit)}
|
|
||||||
</span>{' '}
|
|
||||||
profit
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
@ -272,25 +258,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Spacer h={5} />
|
|
||||||
{currentUser?.id === user.id && (
|
|
||||||
<Row
|
|
||||||
className={
|
|
||||||
'w-full items-center justify-center gap-2 rounded-md border-2 border-indigo-100 bg-indigo-50 p-2 text-indigo-600'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Refer a friend and earn {formatMoney(500)} when they sign up! You
|
|
||||||
have <ReferralsButton user={user} currentUser={currentUser} />
|
|
||||||
</span>
|
|
||||||
<ShareIconButton
|
|
||||||
copyPayload={`https://${ENV_CONFIG.domain}?referrer=${currentUser.username}`}
|
|
||||||
toastClassName={'sm:-left-40 -left-40 min-w-[250%]'}
|
|
||||||
buttonClassName={'h-10 w-10'}
|
|
||||||
iconClassName={'h-8 w-8 text-indigo-700'}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
<Spacer h={5} />
|
<Spacer h={5} />
|
||||||
|
|
||||||
{usersContracts !== 'loading' && contractsById && usersComments ? (
|
{usersContracts !== 'loading' && contractsById && usersComments ? (
|
||||||
|
@ -322,23 +289,33 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
...(isCurrentUser ||
|
||||||
title: 'Bets',
|
[
|
||||||
content: (
|
...(ENV_CONFIG.whitelistCreators ?? []),
|
||||||
<div>
|
'JamesGrugett',
|
||||||
<PortfolioValueSection
|
].includes(currentUser?.username ?? '')
|
||||||
portfolioHistory={portfolioHistory}
|
? [
|
||||||
/>
|
{
|
||||||
<BetsList
|
title: 'Bets',
|
||||||
user={user}
|
content: (
|
||||||
bets={userBets}
|
<div>
|
||||||
hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022}
|
<PortfolioValueSection
|
||||||
contractsById={contractsById}
|
portfolioHistory={portfolioHistory}
|
||||||
/>
|
/>
|
||||||
</div>
|
<BetsList
|
||||||
),
|
user={user}
|
||||||
tabIcon: <span className="px-0.5 font-bold">{betCount}</span>,
|
bets={userBets}
|
||||||
},
|
hideBetsBefore={isCurrentUser ? 0 : JUNE_1_2022}
|
||||||
|
contractsById={contractsById}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
tabIcon: (
|
||||||
|
<span className="px-0.5 font-bold">{betCount}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { defaults, debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { DEFAULT_SORT } from 'web/components/contract-search'
|
import { DEFAULT_SORT } from 'web/components/contract-search'
|
||||||
|
@ -25,53 +25,6 @@ export function getSavedSort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialQueryAndSort(options?: {
|
|
||||||
defaultSort: Sort
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}) {
|
|
||||||
const { defaultSort, shouldLoadFromStorage } = defaults(options, {
|
|
||||||
defaultSort: DEFAULT_SORT,
|
|
||||||
shouldLoadFromStorage: true,
|
|
||||||
})
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const [initialSort, setInitialSort] = useState<Sort | undefined>(undefined)
|
|
||||||
const [initialQuery, setInitialQuery] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If there's no sort option, then set the one from localstorage
|
|
||||||
if (router.isReady) {
|
|
||||||
const { s: sort, q: query } = router.query as {
|
|
||||||
q?: string
|
|
||||||
s?: Sort
|
|
||||||
}
|
|
||||||
|
|
||||||
setInitialQuery(query ?? '')
|
|
||||||
|
|
||||||
if (!sort && shouldLoadFromStorage) {
|
|
||||||
console.log('ready loading from storage ', sort ?? defaultSort)
|
|
||||||
const localSort = getSavedSort()
|
|
||||||
if (localSort) {
|
|
||||||
// Use replace to not break navigating back.
|
|
||||||
router.replace(
|
|
||||||
{ query: { ...router.query, s: localSort } },
|
|
||||||
undefined,
|
|
||||||
{ shallow: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setInitialSort(localSort ?? defaultSort)
|
|
||||||
} else {
|
|
||||||
setInitialSort(sort ?? defaultSort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [defaultSort, router.isReady, shouldLoadFromStorage])
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialSort,
|
|
||||||
initialQuery,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useQueryAndSortParams(options?: {
|
export function useQueryAndSortParams(options?: {
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
|
|
|
@ -213,14 +213,15 @@ export async function listAllUsers() {
|
||||||
return docs.map((doc) => doc.data())
|
return docs.map((doc) => doc.data())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopTraders(period: Period) {
|
export async function getTopTraders(period: Period) {
|
||||||
const topTraders = query(
|
const topTraders = query(
|
||||||
users,
|
users,
|
||||||
orderBy('profitCached.' + period, 'desc'),
|
orderBy('profitCached.' + period, 'desc'),
|
||||||
limit(20)
|
limit(21)
|
||||||
)
|
)
|
||||||
|
|
||||||
return getValues<User>(topTraders)
|
const topUsers = await getValues<User>(topTraders)
|
||||||
|
return topUsers.slice(0, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopCreators(period: Period) {
|
export function getTopCreators(period: Period) {
|
||||||
|
@ -236,6 +237,20 @@ export async function getTopFollowed() {
|
||||||
return (await getValues<User>(topFollowedQuery)).slice(0, 20)
|
return (await getValues<User>(topFollowedQuery)).slice(0, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFirstDayProfit(userId: string) {
|
||||||
|
const firstDay = new Date('2022-08-08').getTime()
|
||||||
|
const firstDayPortfolio = query(
|
||||||
|
collection(users, userId, 'portfolioHistory'),
|
||||||
|
where('timestamp', '<', firstDay),
|
||||||
|
orderBy('timestamp', 'desc'),
|
||||||
|
limit(1)
|
||||||
|
)
|
||||||
|
const values = await getValues<PortfolioMetrics>(firstDayPortfolio)
|
||||||
|
if (values.length === 0) return 0
|
||||||
|
const portfolioValue = values[0].balance + values[0].investmentValue
|
||||||
|
return Math.max(0, portfolioValue - 1000)
|
||||||
|
}
|
||||||
|
|
||||||
const topFollowedQuery = query(
|
const topFollowedQuery = query(
|
||||||
users,
|
users,
|
||||||
orderBy('followerCountCached', 'desc'),
|
orderBy('followerCountCached', 'desc'),
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"",
|
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"",
|
||||||
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev",
|
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev",
|
||||||
"dev:dev": "yarn devdev",
|
"dev:dev": "yarn devdev",
|
||||||
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE yarn dev",
|
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"",
|
||||||
"dev:local": "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8080 yarn devdev",
|
"dev:salem": "cross-env NEXT_PUBLIC_FIREBASE_ENV=SALEM_CENTER concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=SALEM_CENTER next dev -p 3000\" \"cross-env FIREBASE_ENV=SALEM_CENTER yarn ts --watch\"",
|
||||||
"dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev",
|
"dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
||||||
import { Comment, listAllComments } from 'web/lib/firebase/comments'
|
import { Comment, listAllComments } from 'web/lib/firebase/comments'
|
||||||
import Custom404 from '../404'
|
import Custom404 from '../404'
|
||||||
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
import { AnswersPanel } from 'web/components/answers/answers-panel'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
|
||||||
import { Leaderboard } from 'web/components/leaderboard'
|
import { Leaderboard } from 'web/components/leaderboard'
|
||||||
import { resolvedPayout } from 'common/calculate'
|
import { resolvedPayout } from 'common/calculate'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
@ -45,8 +44,7 @@ import { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export async function getStaticProps(props: {
|
||||||
export async function getStaticPropz(props: {
|
|
||||||
params: { username: string; contractSlug: string }
|
params: { username: string; contractSlug: string }
|
||||||
}) {
|
}) {
|
||||||
const { username, contractSlug } = props.params
|
const { username, contractSlug } = props.params
|
||||||
|
@ -84,14 +82,6 @@ export default function ContractPage(props: {
|
||||||
slug: string
|
slug: string
|
||||||
backToHome?: () => void
|
backToHome?: () => void
|
||||||
}) {
|
}) {
|
||||||
props = usePropz(props, getStaticPropz) ?? {
|
|
||||||
contract: null,
|
|
||||||
username: '',
|
|
||||||
comments: [],
|
|
||||||
bets: [],
|
|
||||||
slug: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const inIframe = useIsIframe()
|
const inIframe = useIsIframe()
|
||||||
if (inIframe) {
|
if (inIframe) {
|
||||||
return <ContractEmbedPage {...props} />
|
return <ContractEmbedPage {...props} />
|
||||||
|
|
|
@ -13,6 +13,8 @@ export async function getStaticPropz(props: { params: { username: string } }) {
|
||||||
const { username } = props.params
|
const { username } = props.params
|
||||||
const user = await getUserByUsername(username)
|
const user = await getUserByUsername(username)
|
||||||
|
|
||||||
|
if (user) user.profitCached.allTime = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
|
@ -38,7 +40,7 @@ export default function UserProfile(props: { user: User | null }) {
|
||||||
|
|
||||||
useTracking('view user profile', { username })
|
useTracking('view user profile', { username })
|
||||||
|
|
||||||
if (user === undefined) return <div />
|
if (user === undefined || user?.username !== username) return <div />
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<UserPage user={user} currentUser={currentUser || undefined} />
|
<UserPage user={user} currentUser={currentUser || undefined} />
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { mapKeys } from 'lodash'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getFirstDayProfit } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
|
|
||||||
|
@ -27,17 +29,50 @@ function UsersTable() {
|
||||||
|
|
||||||
// Map private users by user id
|
// Map private users by user id
|
||||||
const privateUsersById = mapKeys(privateUsers, 'id')
|
const privateUsersById = mapKeys(privateUsers, 'id')
|
||||||
console.log('private users by id', privateUsersById)
|
|
||||||
|
const [profitByUser, setProfitByUser] = useState<{
|
||||||
|
[userId: string]: number
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all(users.map((user) => getFirstDayProfit(user.id))).then(
|
||||||
|
(firstDayProfits) => {
|
||||||
|
setProfitByUser(
|
||||||
|
Object.fromEntries(
|
||||||
|
users.map((user, i) => [
|
||||||
|
user.id,
|
||||||
|
user.profitCached.allTime - firstDayProfits[i],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [users.map((user) => user.id).join(',')])
|
||||||
|
|
||||||
// For each user, set their email from the PrivateUser
|
// For each user, set their email from the PrivateUser
|
||||||
const fullUsers = users
|
const fullUsers = users
|
||||||
.map((user) => {
|
.map((user) => {
|
||||||
return { email: privateUsersById[user.id]?.email, ...user }
|
return {
|
||||||
|
email: privateUsersById[user.id]?.email,
|
||||||
|
profit: profitByUser[user.id] ?? 0,
|
||||||
|
ip: privateUsersById[user.id]?.initialIpAddress,
|
||||||
|
...user,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.createdTime - a.createdTime)
|
.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
|
|
||||||
function exportCsv() {
|
function exportCsv() {
|
||||||
const csv = fullUsers.map((u) => [u.email, u.name].join(', ')).join('\n')
|
const lines = [['Email', 'Name', 'Balance', 'Profit', 'IP Address']].concat(
|
||||||
|
fullUsers.map((u) => [
|
||||||
|
u.email ?? '',
|
||||||
|
u.name,
|
||||||
|
Math.round(u.balance).toString(),
|
||||||
|
Math.round(profitByUser[u.id] ?? 0).toString(),
|
||||||
|
u.ip ?? '',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
const csv = lines.map((line) => line.join(', ')).join('\n')
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
|
@ -90,6 +125,11 @@ function UsersTable() {
|
||||||
name: 'Balance',
|
name: 'Balance',
|
||||||
formatter: (cell) => (cell as number).toFixed(0),
|
formatter: (cell) => (cell as number).toFixed(0),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'profit',
|
||||||
|
name: 'profit',
|
||||||
|
formatter: (cell) => (cell as number).toFixed(0),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'ID',
|
name: 'ID',
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import {
|
|
||||||
CORS_ORIGIN_MANIFOLD,
|
|
||||||
CORS_ORIGIN_LOCALHOST,
|
|
||||||
} from 'common/envs/constants'
|
|
||||||
import { applyCorsHeaders } from 'web/lib/api/cors'
|
|
||||||
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
|
|
||||||
|
|
||||||
export const config = { api: { bodyParser: true } }
|
|
||||||
|
|
||||||
export default async function route(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
await applyCorsHeaders(req, res, {
|
|
||||||
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
|
||||||
methods: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { betId } = req.query as { betId: string }
|
|
||||||
|
|
||||||
if (req.body) req.body.betId = betId
|
|
||||||
try {
|
|
||||||
const backendRes = await fetchBackend(req, 'cancelbet')
|
|
||||||
await forwardResponse(res, backendRes)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error talking to cloud function: ', err)
|
|
||||||
res.status(500).json({ message: 'Error communicating with backend.' })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import {
|
|
||||||
CORS_ORIGIN_MANIFOLD,
|
|
||||||
CORS_ORIGIN_LOCALHOST,
|
|
||||||
} from 'common/envs/constants'
|
|
||||||
import { applyCorsHeaders } from 'web/lib/api/cors'
|
|
||||||
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
|
|
||||||
|
|
||||||
export const config = { api: { bodyParser: false } }
|
|
||||||
|
|
||||||
export default async function route(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
await applyCorsHeaders(req, res, {
|
|
||||||
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
|
||||||
methods: 'POST',
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const backendRes = await fetchBackend(req, 'placebet')
|
|
||||||
await forwardResponse(res, backendRes)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error talking to cloud function: ', err)
|
|
||||||
res.status(500).json({ message: 'Error communicating with backend.' })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
import { Bet, getBets } from 'web/lib/firebase/bets'
|
|
||||||
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
|
||||||
import { getUserByUsername } from 'web/lib/firebase/users'
|
|
||||||
import { ApiError, ValidationError } from './_types'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { validate } from './_validate'
|
|
||||||
|
|
||||||
const queryParams = z
|
|
||||||
.object({
|
|
||||||
username: z.string().optional(),
|
|
||||||
market: z.string().optional(),
|
|
||||||
limit: z
|
|
||||||
.number()
|
|
||||||
.default(1000)
|
|
||||||
.or(z.string().regex(/\d+/).transform(Number))
|
|
||||||
.refine((n) => n >= 0 && n <= 1000, 'Limit must be between 0 and 1000'),
|
|
||||||
before: z.string().optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Bet[] | ValidationError | ApiError>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
|
|
||||||
let params: z.infer<typeof queryParams>
|
|
||||||
try {
|
|
||||||
params = validate(queryParams, req.query)
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ValidationError) {
|
|
||||||
return res.status(400).json(e)
|
|
||||||
}
|
|
||||||
console.error(`Unknown error during validation: ${e}`)
|
|
||||||
return res.status(500).json({ error: 'Unknown error during validation' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username, market, limit, before } = params
|
|
||||||
|
|
||||||
let userId: string | undefined
|
|
||||||
if (username) {
|
|
||||||
const user = await getUserByUsername(username)
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ error: 'User not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userId = user.id
|
|
||||||
}
|
|
||||||
|
|
||||||
let contractId: string | undefined
|
|
||||||
if (market) {
|
|
||||||
const contract = await getContractFromSlug(market)
|
|
||||||
if (!contract) {
|
|
||||||
res.status(404).json({ error: 'Contract not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contractId = contract.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const bets = await getBets({ userId, contractId, limit, before })
|
|
||||||
|
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
|
||||||
return res.status(200).json(bets)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { getGroupBySlug } from 'web/lib/firebase/groups'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const { slug } = req.query
|
|
||||||
const group = await getGroupBySlug(slug as string)
|
|
||||||
if (!group) {
|
|
||||||
res.status(404).json({ error: 'Group not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
|
||||||
return res.status(200).json(group)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { getGroup } from 'web/lib/firebase/groups'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const { id } = req.query
|
|
||||||
const group = await getGroup(id as string)
|
|
||||||
if (!group) {
|
|
||||||
res.status(404).json({ error: 'Group not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
|
||||||
return res.status(200).json(group)
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { listAllGroups } from 'web/lib/firebase/groups'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
|
|
||||||
type Data = any[]
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const groups = await listAllGroups()
|
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
|
||||||
res.status(200).json(groups)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import {
|
|
||||||
CORS_ORIGIN_MANIFOLD,
|
|
||||||
CORS_ORIGIN_LOCALHOST,
|
|
||||||
} from 'common/envs/constants'
|
|
||||||
import { applyCorsHeaders } from 'web/lib/api/cors'
|
|
||||||
import { fetchBackend, forwardResponse } from 'web/lib/api/proxy'
|
|
||||||
|
|
||||||
export const config = { api: { bodyParser: true } }
|
|
||||||
|
|
||||||
export default async function route(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
await applyCorsHeaders(req, res, {
|
|
||||||
origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST],
|
|
||||||
methods: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { id } = req.query
|
|
||||||
const contractId = id as string
|
|
||||||
|
|
||||||
if (req.body) req.body.contractId = contractId
|
|
||||||
try {
|
|
||||||
const backendRes = await fetchBackend(req, 'sellshares')
|
|
||||||
await forwardResponse(res, backendRes)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error talking to cloud function: ', err)
|
|
||||||
res.status(500).json({ message: 'Error communicating with backend.' })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
import { Bet, getUserBets } from 'web/lib/firebase/bets'
|
|
||||||
import { getUserByUsername } from 'web/lib/firebase/users'
|
|
||||||
import { ApiError } from '../../../_types'
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Bet[] | ApiError>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const { username } = req.query
|
|
||||||
|
|
||||||
const user = await getUserByUsername(username as string)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ error: 'User not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const bets = await getUserBets(user.id, { includeRedemptions: false })
|
|
||||||
|
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
|
||||||
return res.status(200).json(bets)
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { getUserByUsername } from 'web/lib/firebase/users'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
import { LiteUser, ApiError, toLiteUser } from '../../_types'
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<LiteUser | ApiError>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const { username } = req.query
|
|
||||||
const user = await getUserByUsername(username as string)
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ error: 'User not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
|
||||||
return res.status(200).json(toLiteUser(user))
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { getUser } from 'web/lib/firebase/users'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
import { LiteUser, ApiError, toLiteUser } from '../../_types'
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<LiteUser | ApiError>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const { id } = req.query
|
|
||||||
const user = await getUser(id as string)
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ error: 'User not found' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
|
||||||
return res.status(200).json(toLiteUser(user))
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
// Next.js API route support: https://vercel.com/docs/concepts/functions/serverless-functions
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { listAllUsers } from 'web/lib/firebase/users'
|
|
||||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
|
||||||
import { toLiteUser } from './_types'
|
|
||||||
|
|
||||||
type Data = any[]
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
await applyCorsHeaders(req, res, CORS_UNRESTRICTED)
|
|
||||||
const users = await listAllUsers()
|
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
|
||||||
res.status(200).json(users.map(toLiteUser))
|
|
||||||
}
|
|
|
@ -33,10 +33,10 @@ export default function CharityPageWrapper() {
|
||||||
if (!charity) {
|
if (!charity) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
return <CharityPage charity={charity} />
|
// return <CharityPage charity={charity} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function CharityPage(props: { charity: Charity }) {
|
function _CharityPage(props: { charity: Charity }) {
|
||||||
const { charity } = props
|
const { charity } = props
|
||||||
const { name, photo, description } = charity
|
const { name, photo, description } = charity
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,7 @@ import { quadraticMatches } from 'common/quadratic-funding'
|
||||||
import { Txn } from 'common/txn'
|
import { Txn } from 'common/txn'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { searchInAny } from 'common/util/parse'
|
import { searchInAny } from 'common/util/parse'
|
||||||
import { getUser } from 'web/lib/firebase/users'
|
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { User } from 'common/user'
|
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
|
@ -37,7 +35,6 @@ export async function getStaticProps() {
|
||||||
])
|
])
|
||||||
const matches = quadraticMatches(txns, totalRaised)
|
const matches = quadraticMatches(txns, totalRaised)
|
||||||
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
||||||
const mostRecentDonor = await getUser(txns[txns.length - 1].fromId)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -46,7 +43,6 @@ export async function getStaticProps() {
|
||||||
matches,
|
matches,
|
||||||
txns,
|
txns,
|
||||||
numDonors,
|
numDonors,
|
||||||
mostRecentDonor,
|
|
||||||
},
|
},
|
||||||
revalidate: 60,
|
revalidate: 60,
|
||||||
}
|
}
|
||||||
|
@ -90,9 +86,8 @@ export default function Charity(props: {
|
||||||
matches: { [charityId: string]: number }
|
matches: { [charityId: string]: number }
|
||||||
txns: Txn[]
|
txns: Txn[]
|
||||||
numDonors: number
|
numDonors: number
|
||||||
mostRecentDonor: User
|
|
||||||
}) {
|
}) {
|
||||||
const { totalRaised, charities, matches, numDonors, mostRecentDonor } = props
|
const { totalRaised, charities, matches, numDonors } = props
|
||||||
|
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const debouncedQuery = debounce(setQuery, 50)
|
const debouncedQuery = debounce(setQuery, 50)
|
||||||
|
@ -149,8 +144,8 @@ export default function Charity(props: {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Most recent donor',
|
name: 'Most recent donor',
|
||||||
stat: mostRecentDonor.name ?? 'Nobody',
|
stat: 'Nobody',
|
||||||
url: `/${mostRecentDonor.username}`,
|
url: `/`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { searchInAny } from 'common/util/parse'
|
import { searchInAny } from 'common/util/parse'
|
||||||
import { sortBy } from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
import { useState } from 'react'
|
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-list'
|
import { ContractsGrid } from 'web/components/contract/contracts-list'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { useContracts } from 'web/hooks/use-contracts'
|
import { useContracts } from 'web/hooks/use-contracts'
|
||||||
import {
|
import {
|
||||||
Sort,
|
Sort,
|
||||||
useInitialQueryAndSort,
|
useQueryAndSortParams,
|
||||||
} from 'web/hooks/use-sort-and-query-params'
|
} from 'web/hooks/use-sort-and-query-params'
|
||||||
|
|
||||||
const MAX_CONTRACTS_RENDERED = 100
|
const MAX_CONTRACTS_RENDERED = 100
|
||||||
|
@ -27,9 +26,8 @@ export default function ContractSearchFirestore(props: {
|
||||||
const contracts = useContracts()
|
const contracts = useContracts()
|
||||||
const { querySortOptions, additionalFilter } = props
|
const { querySortOptions, additionalFilter } = props
|
||||||
|
|
||||||
const { initialSort, initialQuery } = useInitialQueryAndSort(querySortOptions)
|
const { query, setQuery, sort, setSort } =
|
||||||
const [sort, setSort] = useState(initialSort || 'newest')
|
useQueryAndSortParams(querySortOptions)
|
||||||
const [query, setQuery] = useState(initialQuery)
|
|
||||||
|
|
||||||
let matches = (contracts ?? []).filter((c) =>
|
let matches = (contracts ?? []).filter((c) =>
|
||||||
searchInAny(
|
searchInAny(
|
||||||
|
@ -49,11 +47,7 @@ export default function ContractSearchFirestore(props: {
|
||||||
matches.sort((a, b) => a.createdTime - b.createdTime)
|
matches.sort((a, b) => a.createdTime - b.createdTime)
|
||||||
} else if (sort === 'close-date') {
|
} else if (sort === 'close-date') {
|
||||||
matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
||||||
matches = sortBy(
|
matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity)
|
||||||
matches,
|
|
||||||
(contract) =>
|
|
||||||
(sort === 'close-date' ? -1 : 1) * (contract.closeTime ?? Infinity)
|
|
||||||
)
|
|
||||||
} else if (sort === 'most-traded') {
|
} else if (sort === 'most-traded') {
|
||||||
matches.sort((a, b) => b.volume - a.volume)
|
matches.sort((a, b) => b.volume - a.volume)
|
||||||
} else if (sort === 'score') {
|
} else if (sort === 'score') {
|
||||||
|
@ -110,9 +104,8 @@ export default function ContractSearchFirestore(props: {
|
||||||
value={sort}
|
value={sort}
|
||||||
onChange={(e) => setSort(e.target.value as Sort)}
|
onChange={(e) => setSort(e.target.value as Sort)}
|
||||||
>
|
>
|
||||||
<option value="newest">Newest</option>
|
|
||||||
<option value="oldest">Oldest</option>
|
|
||||||
<option value="score">Trending</option>
|
<option value="score">Trending</option>
|
||||||
|
<option value="newest">Newest</option>
|
||||||
<option value="most-traded">Most traded</option>
|
<option value="most-traded">Most traded</option>
|
||||||
<option value="24-hour-vol">24h volume</option>
|
<option value="24-hour-vol">24h volume</option>
|
||||||
<option value="close-date">Closing soon</option>
|
<option value="close-date">Closing soon</option>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { GroupSelector } from 'web/components/groups/group-selector'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { TextEditor, useTextEditor } from 'web/components/editor'
|
import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
import { Checkbox } from 'web/components/checkbox'
|
import { Checkbox } from 'web/components/checkbox'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
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'
|
||||||
|
@ -61,6 +62,16 @@ export default function Create() {
|
||||||
}, [params.q])
|
}, [params.q])
|
||||||
|
|
||||||
const creator = useUser()
|
const creator = useUser()
|
||||||
|
useEffect(() => {
|
||||||
|
if (creator === null) router.push('/')
|
||||||
|
if (
|
||||||
|
ENV_CONFIG.whitelistCreators &&
|
||||||
|
!ENV_CONFIG.whitelistCreators?.includes(creator?.username ?? '')
|
||||||
|
) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}, [creator, router])
|
||||||
|
|
||||||
if (!router.isReady || !creator) return <div />
|
if (!router.isReady || !creator) return <div />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -111,7 +122,8 @@ export function NewContract(props: {
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
||||||
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
||||||
)
|
)
|
||||||
const [initialProb] = useState(50)
|
const [initialProb, setInitialProb] = useState(50)
|
||||||
|
const [probErrorText, setProbErrorText] = useState('')
|
||||||
const [minString, setMinString] = useState(params?.min ?? '')
|
const [minString, setMinString] = useState(params?.min ?? '')
|
||||||
const [maxString, setMaxString] = useState(params?.max ?? '')
|
const [maxString, setMaxString] = useState(params?.max ?? '')
|
||||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||||
|
@ -283,6 +295,56 @@ export function NewContract(props: {
|
||||||
|
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
{outcomeType === 'BINARY' && (
|
||||||
|
<div className="form-control">
|
||||||
|
<Row className="label justify-start">
|
||||||
|
<span className="mb-1">How likely is it to happen?</span>
|
||||||
|
</Row>
|
||||||
|
<Row className={'justify-start'}>
|
||||||
|
<ChoicesToggleGroup
|
||||||
|
currentChoice={initialProb}
|
||||||
|
setChoice={(option) => {
|
||||||
|
setProbErrorText('')
|
||||||
|
setInitialProb(option as number)
|
||||||
|
}}
|
||||||
|
choicesMap={{
|
||||||
|
Unlikely: 25,
|
||||||
|
'Not Sure': 50,
|
||||||
|
Likely: 75,
|
||||||
|
}}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
className={'col-span-4 sm:col-span-3'}
|
||||||
|
>
|
||||||
|
<Row className={'col-span-3 items-center justify-start'}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={initialProb}
|
||||||
|
className={
|
||||||
|
'input-bordered input-md max-w-[100px] rounded-md border-gray-300 pr-2 text-lg'
|
||||||
|
}
|
||||||
|
min={5}
|
||||||
|
max={95}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onChange={(e) => {
|
||||||
|
// show error if prob is less than 5 or greater than 95:
|
||||||
|
const prob = parseInt(e.target.value)
|
||||||
|
setInitialProb(prob)
|
||||||
|
if (prob < 5 || prob > 95)
|
||||||
|
setProbErrorText('Probability must be between 5% and 95%')
|
||||||
|
else setProbErrorText('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className={'ml-1'}>%</span>
|
||||||
|
</Row>
|
||||||
|
</ChoicesToggleGroup>
|
||||||
|
</Row>
|
||||||
|
{probErrorText && (
|
||||||
|
<div className="text-error mt-2 ml-1 text-sm">{probErrorText}</div>
|
||||||
|
)}
|
||||||
|
<Spacer h={6} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'MULTIPLE_CHOICE' && (
|
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||||
<MultipleChoiceAnswers setAnswers={setAnswers} />
|
<MultipleChoiceAnswers setAnswers={setAnswers} />
|
||||||
)}
|
)}
|
||||||
|
@ -448,12 +510,6 @@ export function NewContract(props: {
|
||||||
{ante > balance && (
|
{ante > balance && (
|
||||||
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
<div className="mb-2 mt-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide">
|
||||||
<span className="mr-2 text-red-500">Insufficient balance</span>
|
<span className="mr-2 text-red-500">Insufficient balance</span>
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-primary"
|
|
||||||
onClick={() => (window.location.href = '/add-funds')}
|
|
||||||
>
|
|
||||||
Get M$
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -160,7 +160,7 @@ export default function GroupPage(props: {
|
||||||
const privateUser = usePrivateUser(user?.id)
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
|
||||||
useSaveReferral(user, {
|
useSaveReferral(user, {
|
||||||
defaultReferrerUsername: creator.username,
|
defaultReferrerUsername: creator?.username,
|
||||||
groupId: group?.id,
|
groupId: group?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { PlusSmIcon } from '@heroicons/react/solid'
|
|
||||||
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
@ -10,7 +8,6 @@ import { Contract } from 'common/contract'
|
||||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
import { ContractPageContent } from './[username]/[contractSlug]'
|
||||||
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { track } from 'web/lib/service/analytics'
|
|
||||||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
|
|
||||||
|
@ -19,7 +16,6 @@ export const getServerSideProps = redirectIfLoggedOut('/')
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [contract, setContract] = useContractPage()
|
const [contract, setContract] = useContractPage()
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
|
||||||
useSaveReferral()
|
useSaveReferral()
|
||||||
|
@ -41,16 +37,6 @@ const Home = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="fixed bottom-[70px] right-3 inline-flex items-center rounded-full border border-transparent bg-indigo-600 p-3 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 lg:hidden"
|
|
||||||
onClick={() => {
|
|
||||||
router.push('/create')
|
|
||||||
track('mobile create button')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusSmIcon className="h-8 w-8" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{contract && (
|
{contract && (
|
||||||
|
|
|
@ -6,6 +6,9 @@ import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
||||||
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
|
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
||||||
// These hardcoded markets will be shown in the frontpage for signed-out users:
|
// These hardcoded markets will be shown in the frontpage for signed-out users:
|
||||||
|
@ -24,11 +27,18 @@ export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
|
||||||
return { props: { hotContracts } }
|
return { props: { hotContracts } }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function Home(props: { hotContracts: Contract[] }) {
|
export default function Home(_props: { hotContracts: Contract[] }) {
|
||||||
const { hotContracts } = props
|
|
||||||
|
|
||||||
useSaveReferral()
|
useSaveReferral()
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const router = useRouter()
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
// Redirect to the /home page if the user is logged in.
|
||||||
|
router.push('/home')
|
||||||
|
}
|
||||||
|
}, [user, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -41,7 +51,7 @@ export default function Home(props: { hotContracts: Contract[] }) {
|
||||||
</div>
|
</div>
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-3xl">
|
<Col className="max-w-3xl">
|
||||||
<LandingPagePanel hotContracts={hotContracts ?? []} />
|
<LandingPagePanel />
|
||||||
{/* <p className="mt-6 text-gray-500">
|
{/* <p className="mt-6 text-gray-500">
|
||||||
View{' '}
|
View{' '}
|
||||||
<SiteLink href="/markets" className="font-bold text-gray-700">
|
<SiteLink href="/markets" className="font-bold text-gray-700">
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Leaderboard } from 'web/components/leaderboard'
|
import { Leaderboard } from 'web/components/leaderboard'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import {
|
import { User, getFirstDayProfit, listAllUsers } from 'web/lib/firebase/users'
|
||||||
getTopCreators,
|
|
||||||
getTopTraders,
|
|
||||||
getTopFollowed,
|
|
||||||
Period,
|
|
||||||
User,
|
|
||||||
} from 'web/lib/firebase/users'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Title } from 'web/components/title'
|
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { sortBy } from 'lodash'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const props = await fetchProps()
|
const props = await fetchProps()
|
||||||
|
@ -25,134 +17,69 @@ export async function getStaticProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchProps = async () => {
|
const fetchProps = async () => {
|
||||||
const [allTime, monthly, weekly, daily] = await Promise.all([
|
const users = await listAllUsers()
|
||||||
queryLeaderboardUsers('allTime'),
|
const firstDayProfit = await Promise.all(
|
||||||
queryLeaderboardUsers('monthly'),
|
users.map((user) => getFirstDayProfit(user.id))
|
||||||
queryLeaderboardUsers('weekly'),
|
)
|
||||||
queryLeaderboardUsers('daily'),
|
const userProfit = users.map(
|
||||||
])
|
(user, i) => [user, user.profitCached.allTime - firstDayProfit[i]] as const
|
||||||
const topFollowed = await getTopFollowed()
|
)
|
||||||
|
const topTradersProfit = sortBy(userProfit, ([_, profit]) => profit)
|
||||||
|
.reverse()
|
||||||
|
.filter(
|
||||||
|
([user]) =>
|
||||||
|
user.username !== 'SalemCenter' &&
|
||||||
|
user.username !== 'RichardHanania' &&
|
||||||
|
user.username !== 'JamesGrugett'
|
||||||
|
)
|
||||||
|
.slice(0, 20)
|
||||||
|
|
||||||
return {
|
const topTraders = topTradersProfit.map(([user]) => user)
|
||||||
allTime,
|
|
||||||
monthly,
|
// Hide profit for now.
|
||||||
weekly,
|
topTraders.forEach((user) => {
|
||||||
daily,
|
user.profitCached.allTime = 0
|
||||||
topFollowed,
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryLeaderboardUsers = async (period: Period) => {
|
|
||||||
const [topTraders, topCreators] = await Promise.all([
|
|
||||||
getTopTraders(period),
|
|
||||||
getTopCreators(period),
|
|
||||||
])
|
|
||||||
return {
|
return {
|
||||||
topTraders,
|
topTraders,
|
||||||
topCreators,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type leaderboard = {
|
export default function Leaderboards(_props: { topTraders: User[] }) {
|
||||||
topTraders: User[]
|
const [{ topTraders }, setProps] =
|
||||||
topCreators: User[]
|
useState<Parameters<typeof Leaderboards>[0]>(_props)
|
||||||
}
|
|
||||||
|
|
||||||
export default function Leaderboards(_props: {
|
|
||||||
allTime: leaderboard
|
|
||||||
monthly: leaderboard
|
|
||||||
weekly: leaderboard
|
|
||||||
daily: leaderboard
|
|
||||||
topFollowed: User[]
|
|
||||||
}) {
|
|
||||||
const [props, setProps] = useState<Parameters<typeof Leaderboards>[0]>(_props)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProps().then((props) => setProps(props))
|
fetchProps().then((props) => setProps(props))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { topFollowed } = props
|
|
||||||
|
|
||||||
const LeaderboardWithPeriod = (period: Period) => {
|
|
||||||
const { topTraders, topCreators } = props[period]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Col className="mx-4 items-center gap-10 lg:flex-row">
|
|
||||||
<Leaderboard
|
|
||||||
title="🏅 Top traders"
|
|
||||||
users={topTraders}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'Total profit',
|
|
||||||
renderCell: (user) => formatMoney(user.profitCached[period]),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Leaderboard
|
|
||||||
title="🏅 Top creators"
|
|
||||||
users={topCreators}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'Total bet',
|
|
||||||
renderCell: (user) =>
|
|
||||||
formatMoney(user.creatorVolumeCached[period]),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
{period === 'allTime' ? (
|
|
||||||
<Col className="mx-4 my-10 items-center gap-10 lg:mx-0 lg:w-1/2 lg:flex-row">
|
|
||||||
<Leaderboard
|
|
||||||
title="🏅 Top followed"
|
|
||||||
users={topFollowed}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'Total followers',
|
|
||||||
renderCell: (user) => user.followerCountCached,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
useTracking('view leaderboards')
|
useTracking('view leaderboards')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
title="Leaderboards"
|
title="Leaderboard"
|
||||||
description="Manifold's leaderboards show the top traders and market creators."
|
description="See the top traders of the CSPI/Salem Center Tournament."
|
||||||
url="/leaderboards"
|
url="/leaderboards"
|
||||||
/>
|
/>
|
||||||
<Title text={'Leaderboards'} className={'hidden md:block'} />
|
|
||||||
<Tabs
|
<Col className="mx-4 max-w-sm items-center gap-10 lg:flex-row">
|
||||||
currentPageForAnalytics={'leaderboards'}
|
<Leaderboard
|
||||||
defaultIndex={1}
|
className="my-4"
|
||||||
tabs={[
|
title="🏅 Top traders"
|
||||||
{
|
users={topTraders}
|
||||||
title: 'All Time',
|
columns={
|
||||||
content: LeaderboardWithPeriod('allTime'),
|
[
|
||||||
},
|
// Hide profit for now.
|
||||||
// TODO: Enable this near the end of July!
|
// {
|
||||||
// {
|
// header: 'Total profit',
|
||||||
// title: 'Monthly',
|
// renderCell: (user) => formatMoney(user.profitCached[period]),
|
||||||
// content: LeaderboardWithPeriod('monthly'),
|
// },
|
||||||
// },
|
]
|
||||||
{
|
}
|
||||||
title: 'Weekly',
|
/>
|
||||||
content: LeaderboardWithPeriod('weekly'),
|
</Col>
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Daily',
|
|
||||||
content: LeaderboardWithPeriod('daily'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { RefreshIcon } from '@heroicons/react/outline'
|
import { RefreshIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { AddFundsButton } from 'web/components/add-funds-button'
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
|
@ -240,7 +239,6 @@ export default function ProfilePage() {
|
||||||
<label className="label">Balance</label>
|
<label className="label">Balance</label>
|
||||||
<Row className="ml-1 items-start gap-4 text-gray-500">
|
<Row className="ml-1 items-start gap-4 text-gray-500">
|
||||||
{formatMoney(user?.balance || 0)}
|
{formatMoney(user?.balance || 0)}
|
||||||
<AddFundsButton />
|
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
BIN
web/public/salem-center/logo.ico
Normal file
BIN
web/public/salem-center/logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
37
web/public/salem-center/salem-center-logo.svg
Normal file
37
web/public/salem-center/salem-center-logo.svg
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="198" height="55" viewBox="0 0 198 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M48.7317 19.1015V12.2723H46.2329V11H52.5954V12.2705H50.1003V19.0997L48.7317 19.1015Z" fill="#9CADB7"/>
|
||||||
|
<path d="M53.9879 19.1015V11H59.224V12.2705H55.3566V14.2872H58.1948V15.5027H55.3566V17.853H59.4423V19.1015H53.9879Z" fill="#9CADB7"/>
|
||||||
|
<path d="M60.5156 19.1015L63.2107 14.8005L60.8018 11H62.3668L64.0528 13.7152L65.6801 11H67.1038L64.7371 14.7987L67.4432 19.0997H65.937L63.9336 15.895L61.9503 19.1015H60.5156Z" fill="#9CADB7"/>
|
||||||
|
<path d="M68.3477 19.1014L71.217 10.9688H72.5637L75.433 19.1014H74.0314L73.3746 17.1746H70.296L69.6282 19.1014H68.3477ZM70.7125 15.9701H72.9691L71.8683 12.6884H71.8463L70.7125 15.9701Z" fill="#9CADB7"/>
|
||||||
|
<path d="M76.0918 17.1966L77.1761 16.6264C77.6567 17.5798 78.4016 18.0381 79.4088 18.0381C80.416 18.0381 81.06 17.7209 81.06 16.9876C81.06 16.2873 80.5555 15.9591 79.2254 15.5649C77.6586 15.1066 76.4 14.6794 76.4 13.1358C76.4 11.7571 77.5283 10.8496 79.1703 10.8496C80.7683 10.8496 81.7113 11.6361 82.203 12.5894L81.2068 13.2788C81.0094 12.9107 80.7128 12.6052 80.3506 12.3969C79.9884 12.1885 79.5751 12.0857 79.1575 12.0999C78.2585 12.0999 77.7448 12.4281 77.7448 13.0166C77.7448 13.7719 78.3246 14.0231 79.651 14.4173C81.1829 14.8646 82.403 15.3688 82.403 16.8904C82.403 18.2141 81.2747 19.2536 79.3464 19.2536C77.7448 19.2499 76.6055 18.4213 76.0918 17.1966Z" fill="#9CADB7"/>
|
||||||
|
<path d="M87.7454 19.1015V11H89.5525L91.721 17.4167H91.7412L93.8455 11H95.6802V19.0997H94.3556V13.0497H94.3335C94.2017 13.6077 94.0406 14.1585 93.851 14.6997L92.2824 19.1015H91.0661L89.5011 14.7125C89.3005 14.1546 89.1356 13.5845 89.0076 13.0057H88.9801V19.1015H87.7454Z" fill="#9CADB7"/>
|
||||||
|
<path d="M97.1425 16.2013C97.1425 13.9793 98.6432 13.1799 99.9348 13.1799C101.38 13.1799 101.993 13.9793 102.344 14.8758L101.204 15.2809C100.975 14.6246 100.559 14.3074 99.9201 14.3074C99.1532 14.3074 98.5074 14.8116 98.5074 16.2013C98.5074 17.4369 99.012 18.1153 99.8981 18.1153C100.511 18.1153 100.938 17.9319 101.256 17.1088L102.373 17.5139C101.957 18.4434 101.272 19.2208 99.8651 19.2208C98.5203 19.2208 97.1425 18.3334 97.1425 16.2013Z" fill="#9CADB7"/>
|
||||||
|
<path d="M103.635 15.0333C103.635 12.3199 105.158 10.8533 107.194 10.8533C109.079 10.8533 109.978 11.8928 110.339 13.3704L109.035 13.7371C108.728 12.6646 108.257 12.1164 107.185 12.1164C105.815 12.1164 105.017 13.1779 105.017 15.0168C105.017 16.9436 105.837 18.0051 107.205 18.0051C108.257 18.0051 108.857 17.4789 109.156 16.2634L110.458 16.5604C110.064 18.1683 109.068 19.2518 107.194 19.2518C105.114 19.2499 103.635 17.8199 103.635 15.0333Z" fill="#9CADB7"/>
|
||||||
|
<path d="M111.653 15.0516C111.653 12.3273 113.263 10.8496 115.322 10.8496C117.38 10.8496 118.969 12.3273 118.969 15.0516C118.969 17.7759 117.369 19.2499 115.322 19.2499C113.274 19.2499 111.653 17.7833 111.653 15.0516ZM117.589 15.0516C117.589 13.1468 116.672 12.1073 115.322 12.1073C113.972 12.1073 113.034 13.1468 113.034 15.0516C113.034 16.9564 113.942 18.0179 115.322 18.0179C116.702 18.0179 117.589 16.9564 117.589 15.0516Z" fill="#9CADB7"/>
|
||||||
|
<path d="M120.525 19.1015V11H122.332L124.501 17.4167H124.523L126.625 11H128.46V19.0997H127.141V13.0497H127.119C126.986 13.6075 126.825 14.1582 126.636 14.6997L125.058 19.0887H123.838L122.271 14.6997C122.072 14.1417 121.907 13.5716 121.78 12.9928H121.758V19.0887L120.525 19.1015Z" fill="#9CADB7"/>
|
||||||
|
<path d="M130.526 19.1015V11H133.591C135.211 11 136.417 11.5262 136.417 13.0167C136.417 14.1423 135.804 14.7125 134.982 14.8757V14.8977C135.978 15.081 136.657 15.642 136.657 16.8795C136.657 18.3682 135.442 19.1015 133.788 19.1015H130.526ZM131.872 14.3843H133.459C134.532 14.3843 135.11 14.0562 135.11 13.2843C135.11 12.5125 134.617 12.2118 133.488 12.2118H131.879L131.872 14.3843ZM131.872 17.919H133.646C134.808 17.919 135.31 17.5523 135.31 16.7365C135.31 15.873 134.654 15.5448 133.503 15.5448H131.872V17.919Z" fill="#9CADB7"/>
|
||||||
|
<path d="M137.897 17.1966L138.98 16.6264C139.462 17.5798 140.207 18.0381 141.214 18.0381C142.221 18.0381 142.865 17.7209 142.865 16.9876C142.865 16.2873 142.361 15.9591 141.031 15.5649C139.464 15.1066 138.205 14.6794 138.205 13.1358C138.205 11.7571 139.332 10.8496 140.976 10.8496C142.574 10.8496 143.517 11.6361 144.008 12.5894L143.012 13.2788C142.815 12.9108 142.518 12.6055 142.156 12.3971C141.794 12.1888 141.38 12.0859 140.963 12.0999C140.064 12.0999 139.55 12.4281 139.55 13.0166C139.55 13.7719 140.13 14.0231 141.454 14.4173C142.988 14.8646 144.216 15.3688 144.216 16.8904C144.216 18.2141 143.087 19.2536 141.159 19.2536C139.55 19.2499 138.411 18.4213 137.897 17.1966Z" fill="#9CADB7"/>
|
||||||
|
<path d="M46.0128 38.436H49.5353C49.5353 39.9998 50.8196 40.7533 52.2579 40.7533C53.5752 40.7533 54.865 40.0585 54.865 38.8521C54.865 37.5981 53.3972 37.2535 51.6342 36.8446C49.1941 36.2433 46.2311 35.5356 46.2311 32.0523C46.2311 28.9503 48.5171 27.2031 52.102 27.2031C55.8226 27.2031 57.9361 29.1868 57.9361 32.4098H54.4889C54.4889 31.0165 53.3385 30.3638 52.0286 30.3638C50.8948 30.3638 49.7445 30.8441 49.7445 31.9038C49.7445 33.0405 51.1516 33.3851 52.8634 33.7958C55.331 34.4191 58.4278 35.1873 58.4278 38.81C58.4278 42.3025 55.6447 43.9983 52.2946 43.9983C48.5721 44.0001 46.0128 41.912 46.0128 38.436Z" fill="white"/>
|
||||||
|
<path d="M73.314 31.1831V43.6497H70.6685L70.3786 42.5332C69.2579 43.5075 67.8186 44.0371 66.3332 44.0219C62.5777 44.0219 59.7836 41.1894 59.7836 37.4219C59.7836 33.6544 62.5777 30.8512 66.3332 30.8512C67.8421 30.8336 69.3036 31.3773 70.4337 32.3766L70.8006 31.1849L73.314 31.1831ZM70.0117 37.4164C70.0117 35.4566 68.5733 33.9972 66.6139 33.9972C64.6545 33.9972 63.2107 35.4639 63.2107 37.4164C63.2107 39.3689 64.6527 40.8356 66.6139 40.8356C68.5752 40.8356 70.0062 39.3799 70.0062 37.4201L70.0117 37.4164Z" fill="white"/>
|
||||||
|
<path d="M75.9303 27.2031H79.3042V43.6756H75.9303V27.2031Z" fill="white"/>
|
||||||
|
<path d="M94.0345 37.4C94.0335 37.7311 94.0115 38.0617 93.9685 38.39H84.5219C84.8155 40.0913 85.9346 41.03 87.5711 41.03C88.7453 41.03 89.7066 40.48 90.2203 39.5817H93.7667C93.3514 40.8912 92.5244 42.0319 91.4085 42.8342C90.2926 43.6365 88.9475 44.0576 87.5729 44.0348C83.8853 44.0348 81.0967 41.1822 81.0967 37.444C81.0967 33.7058 83.867 30.844 87.5729 30.844C91.4055 30.844 94.0345 33.7975 94.0345 37.3908V37.4ZM84.61 36.0855H90.6643C90.1873 34.5602 89.092 33.7333 87.5766 33.7333C86.0612 33.7333 85.0026 34.6042 84.61 36.08V36.0855Z" fill="white"/>
|
||||||
|
<path d="M114.408 35.8527V43.6755H111.034V36.4101C111.034 34.7454 110.379 33.8672 109.157 33.8672C107.649 33.8672 106.756 34.9764 106.756 36.9289V43.6755H103.474V36.4101C103.474 34.7454 102.839 33.8672 101.63 33.8672C100.103 33.8672 99.1917 34.9764 99.1917 36.9289V43.6755H95.8104V31.2089H98.1679L98.8009 32.7544C99.2437 32.1883 99.8081 31.7289 100.452 31.41C101.097 31.0911 101.804 30.9209 102.523 30.9119C104.112 30.9119 105.424 31.6544 106.149 32.9084C106.585 32.2904 107.164 31.7864 107.837 31.4389C108.509 31.0914 109.255 30.9107 110.012 30.9119C112.596 30.9046 114.408 32.9304 114.408 35.8527Z" fill="white"/>
|
||||||
|
<path d="M129.421 44.0257C124.669 44.0257 121.064 40.3866 121.064 35.6107C121.064 30.7964 124.669 27.1572 129.421 27.1572C133.36 27.1572 136.501 29.7606 137.251 33.5977H133.745C133.083 31.6892 131.465 30.4957 129.425 30.4957C126.686 30.4957 124.616 32.6957 124.616 35.6089C124.616 38.5221 126.686 40.6854 129.425 40.6854C131.553 40.6854 133.195 39.4021 133.777 37.3524H137.306C136.573 41.3509 133.432 44.0239 129.417 44.0239L129.421 44.0257Z" fill="white"/>
|
||||||
|
<path d="M151.332 37.4C151.332 37.731 151.31 38.0617 151.268 38.39H141.818C142.111 40.0913 143.231 41.03 144.869 41.03C146.041 41.03 147.004 40.48 147.516 39.5817H151.064C150.649 40.8912 149.821 42.0318 148.705 42.834C147.589 43.6363 146.244 44.0574 144.869 44.0348C141.183 44.0348 138.393 41.1822 138.393 37.444C138.393 33.7058 141.163 30.844 144.869 30.844C148.701 30.844 151.33 33.7975 151.33 37.3908L151.332 37.4ZM141.908 36.0855H147.962C147.485 34.5602 146.388 33.7333 144.874 33.7333C143.361 33.7333 142.3 34.6042 141.908 36.08V36.0855Z" fill="white"/>
|
||||||
|
<path d="M164.791 36.1166V43.6754H161.415V36.6666C161.415 34.7819 160.698 33.8726 159.238 33.8726C157.498 33.8726 156.436 35.1174 156.436 37.1249V43.6809H153.062V31.2142H155.378L156.034 32.7561C156.549 32.1678 157.185 31.6989 157.9 31.3823C158.615 31.0656 159.39 30.9088 160.171 30.9227C163.017 30.9044 164.791 32.9577 164.791 36.1166Z" fill="white"/>
|
||||||
|
<path d="M174.817 40.6725V43.6755H172.42C169.668 43.6755 167.985 41.9907 167.985 39.2224V33.9167H165.729V33.1834L170.658 27.9456H171.304V31.2016H174.748V33.9167H171.355V38.775C171.355 39.9795 172.049 40.6725 173.278 40.6725H174.817Z" fill="white"/>
|
||||||
|
<path d="M188.73 37.4C188.729 37.731 188.708 38.0617 188.665 38.39H179.217C179.51 40.0913 180.63 41.03 182.268 41.03C183.44 41.03 184.403 40.48 184.915 39.5817H188.464C188.048 40.8912 187.22 42.0318 186.104 42.834C184.988 43.6363 183.643 44.0574 182.268 44.0348C178.582 44.0348 175.792 41.1822 175.792 37.444C175.792 33.7058 178.562 30.844 182.268 30.844C186.1 30.844 188.73 33.7975 188.73 37.3908V37.4ZM179.305 36.0855H185.359C184.882 34.5602 183.785 33.7333 182.272 33.7333C180.758 33.7333 179.698 34.6042 179.305 36.08V36.0855Z" fill="white"/>
|
||||||
|
<path d="M198 31.1318V34.3145H196.532C194.63 34.3145 193.878 35.156 193.878 37.1947V43.6755H190.504V31.2088H192.669L193.267 32.7378C194.156 31.6378 195.285 31.1392 196.888 31.1392L198 31.1318Z" fill="white"/>
|
||||||
|
<path d="M11.0078 11H0L11.0078 0V11Z" fill="white"/>
|
||||||
|
<path d="M33.0236 0H22.0157V11H33.0236V0Z" fill="#F8971F"/>
|
||||||
|
<path d="M11.0078 11L22.0157 0V11H11.0078Z" fill="#87581C"/>
|
||||||
|
<path d="M11.0078 11H0V22H11.0078V11Z" fill="#F8971F"/>
|
||||||
|
<path d="M11.0078 33L0 22H11.0078V33Z" fill="#87581C"/>
|
||||||
|
<path d="M22.0157 22H11.0078V33H22.0157V22Z" fill="white"/>
|
||||||
|
<path d="M33.0236 33H22.0157V22L33.0236 33Z" fill="#234E8C"/>
|
||||||
|
<path d="M33.0236 33H22.0157V44H33.0236V33Z" fill="#2F83FF"/>
|
||||||
|
<path d="M11.0078 44H0V55H11.0078V44Z" fill="#2F83FF"/>
|
||||||
|
<path d="M11.0078 55V44H22.0157L11.0078 55Z" fill="#234E8C"/>
|
||||||
|
<path d="M22.0157 55V44H33.0236L22.0157 55Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.8 KiB |
Loading…
Reference in New Issue
Block a user