Compare commits

...

65 Commits

Author SHA1 Message Date
James Grugett
fe77615111 Remove propz stuff that was breaking meta tags 2022-09-20 19:32:36 -05:00
James Grugett
66b63881a1 Use configured domain in SEO tag 2022-09-11 14:10:40 -05:00
James Grugett
1e2593894c Allow admins to see bets page 2022-09-04 00:03:04 -05:00
James Grugett
3b390f758f Add ip address to csv 2022-09-03 23:52:33 -05:00
James Grugett
b8b2778b69 Fix flash that reveals other users' profit 2022-08-22 18:27:56 -05:00
James Grugett
d081f5215d Show current user profit on bets page 2022-08-17 15:57:37 -05:00
James Grugett
b56cfd510f Fix csv profit 2022-08-14 20:21:27 -05:00
James Grugett
f407737f0c Move Rules out of more menu into sidebar 2022-08-14 17:40:19 -05:00
James Grugett
4ab86963e8 Fix admin page 2022-08-12 17:38:40 -05:00
James Grugett
1dc94114e3 Delete subset of api 2022-08-12 14:53:51 -05:00
James Grugett
f500064e4e Revert "Delete api"
This reverts commit f32d207178.
2022-08-12 14:51:46 -05:00
James Grugett
e4a2f8acbb Rearrange nav, add Rules link, singular leaderboard 2022-08-12 12:37:15 -05:00
James Grugett
a266de380f Remove console.log 2022-08-12 12:02:39 -05:00
James Grugett
f32d207178 Delete api 2022-08-12 11:59:31 -05:00
James Grugett
919af972c8 Add back input for setting initial probability of created markets 2022-08-12 00:17:30 -05:00
James Grugett
8f157946fa Update admin page to show profit minus profit from first day 2022-08-11 23:18:54 -05:00
James Grugett
f6bb1b9e38 Remove me from leaderboard 2022-08-11 23:04:54 -05:00
James Grugett
9d51f7a662 Update leaderboards to subtract profit of first day 2022-08-11 22:48:50 -05:00
James Grugett
960b118e90 Add discord link 2022-08-11 12:50:45 -05:00
James Grugett
d70838c09b Document cancel bet 2022-08-10 18:42:44 -05:00
James Grugett
adbf06e889 Set amplitude api key 2022-08-10 14:57:00 -05:00
James Grugett
dc44df3968 Disable erroring code on createuser to fix no redirect. Disable welcome email. 2022-08-08 23:00:01 -05:00
James Grugett
3878b08d77 Hide profit from user page 2022-08-08 22:29:10 -05:00
James Grugett
9cc1a5199b Don't show profit on leaderboards 2022-08-08 21:50:15 -05:00
James Grugett
272d5ec2e2 Add external link to Manifold in more menu 2022-08-07 23:39:48 -07:00
James Grugett
b6b670214d Fix query params 2022-08-07 16:50:12 -07:00
James Grugett
a9120312af Default to trending. Fix close date being opposite 2022-08-07 16:43:53 -07:00
James Grugett
bf13e2fc12 Add profit and balance to admin csv export 2022-08-07 16:12:17 -07:00
James Grugett
367046a6e0 Only show liquidity panel to Hanania 2022-08-07 16:04:50 -07:00
James Grugett
c921590643 Fix error 2022-08-07 12:49:39 -07:00
James Grugett
0066589569 Update landing page to mention tournament. 2022-08-07 09:04:31 -07:00
James Grugett
6f196c7518 Use S$ 2022-08-07 08:23:49 -07:00
James Grugett
166e228032 Add hanania email as admin 2022-08-07 08:11:31 -07:00
James Grugett
76923c773e Hide referral panel 2022-08-07 07:51:22 -07:00
James Grugett
90c707516b Redirect home on sign in 2022-08-07 07:41:31 -07:00
James Grugett
ffa74c8e10 Fix signed out sidebar 2022-08-07 07:36:10 -07:00
James Grugett
13a2877e02 Disable non-all time leaderboards 2022-08-07 07:33:14 -07:00
James Grugett
96abf43977 Disable referrals 2022-08-07 07:16:03 -07:00
James Grugett
24655c0d7f Disable challenges 2022-08-06 17:53:53 -07:00
James Grugett
1aed9bb364 Merge branch 'main' into salemcenter 2022-08-05 14:12:34 -07:00
James Grugett
b8cdb6104d Fix group page error 2022-08-02 18:20:10 -07:00
James Grugett
0f28fe3302 Exclude SalemCenter from leaderboards 2022-08-02 18:11:02 -07:00
James Grugett
f61c2c2cc0 Remove top creators leaderboard 2022-07-27 10:38:58 -07:00
James Grugett
6be321fb88 Remove ability to tip comments 2022-07-27 10:36:32 -07:00
James Grugett
bd5d3d2afc Try to fix charity error 2 2022-07-27 10:30:58 -07:00
James Grugett
b24035f0a8 Try to fix charity error 2022-07-27 10:22:52 -07:00
James Grugett
09d95f2b2b Hide charity page to fix error 2022-07-27 09:54:37 -07:00
James Grugett
f62b1037ff Remove unused vars 2022-07-27 09:51:28 -07:00
James Grugett
7673b32a0c Remove add funds buttons 2022-07-27 09:40:48 -07:00
James Grugett
f6c1f46229 Remove groups from sidebar, about, and duplicate leaderboards item 2022-07-27 09:35:01 -07:00
James Grugett
5bfd7d80b0 Merge branch 'main' into salemcenter 2022-07-27 09:26:25 -07:00
James Grugett
efcfa10323 Put back more menu so users can sign out 2022-07-15 01:08:41 -05:00
James Grugett
02aa13f7d1 Whitelist SalemCenter as a market creator as well. 2022-07-15 00:58:39 -05:00
James Grugett
c15292e2cb Remove top creators and top followed from leaderboards 2022-07-15 00:50:52 -05:00
James Grugett
bfc1f38477 Add leaderboards as top level navigation. Remove more option 2022-07-15 00:49:27 -05:00
James Grugett
240355f717 Merge branch 'main' into salemcenter 2022-07-15 00:46:01 -05:00
James Grugett
6a5fe931dd Only RichardHanania can create questions 2022-07-15 00:42:34 -05:00
James Grugett
a6f4a5fc22 Add salem center logo & favicon 2022-07-15 00:32:57 -05:00
James Grugett
d3b78eeb42 Add me as admin in firestore.rules 2022-07-15 00:10:43 -05:00
James Grugett
b7b6e10968 Don't show no portfolio history label 2022-07-14 23:57:25 -05:00
James Grugett
471e55665d Add dev script 2022-07-14 23:55:44 -05:00
James Grugett
c63070744d Add cloudRunId 2022-07-14 23:55:35 -05:00
James Grugett
d9d2dee576 Update indices 2022-07-14 23:26:40 -05:00
James Grugett
133ef04826 Add .env config for functions 2022-07-14 23:14:41 -05:00
James Grugett
0f77b08319 Add Salem Center contants 2022-07-14 23:13:35 -05:00
49 changed files with 472 additions and 785 deletions

View File

@ -1,3 +1,5 @@
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
export type Challenge = {
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
// Also functions as the unique id for the link.
@ -60,4 +62,4 @@ export type Acceptance = {
createdTime: number
}
export const CHALLENGES_ENABLED = true
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD

View File

@ -2,6 +2,7 @@ import { escapeRegExp } from 'lodash'
import { DEV_CONFIG } from './dev'
import { EnvConfig, PROD_CONFIG } from './prod'
import { THEOREMONE_CONFIG } from './theoremone'
import { SALEM_CENTER_CONFIG } from './salemcenter'
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
@ -9,6 +10,7 @@ const CONFIGS: { [env: string]: EnvConfig } = {
PROD: PROD_CONFIG,
DEV: DEV_CONFIG,
THEOREMONE: THEOREMONE_CONFIG,
SALEM_CENTER: SALEM_CENTER_CONFIG,
}
export const ENV_CONFIG = CONFIGS[ENV]

View File

@ -18,6 +18,7 @@ export type EnvConfig = {
faviconPath?: string // Should be a file in /public
navbarLogoPath?: string
newQuestionPlaceholders: string[]
whitelistCreators?: string[]
// Currency controls
fixedAnte?: number

View 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'],
}

View File

@ -528,6 +528,10 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
"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`
Creates a new market on behalf of the authorized user.

View File

@ -22,6 +22,20 @@
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",

View File

@ -10,7 +10,8 @@ service cloud.firestore {
'akrolsmir@gmail.com',
'jahooma@gmail.com',
'taowell@gmail.com',
'manticmarkets@gmail.com'
'manticmarkets@gmail.com',
'richardh0828@gmail.com',
]
}

View File

@ -1,3 +1,3 @@
# This sets which EnvConfig is deployed to Firebase Cloud Functions
NEXT_PUBLIC_FIREBASE_ENV=PROD
NEXT_PUBLIC_FIREBASE_ENV=SALEM_CENTER

View File

@ -16,7 +16,6 @@ import {
cleanDisplayName,
cleanUsername,
} from '../../common/util/clean-username'
import { sendWelcomeEmail } from './emails'
import { isWhitelisted } from '../../common/envs/constants'
import {
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 addUserToDefaultGroups(user)
await sendWelcomeEmail(user, privateUser)
// await addUserToDefaultGroups(user)
// await sendWelcomeEmail(user, privateUser)
await track(auth.uid, 'create user', { username }, { ip: req.ip })
return user
@ -121,7 +120,7 @@ export const numberUsersWithIp = async (ipAddress: string) => {
return snap.docs.length
}
const addUserToDefaultGroups = async (user: User) => {
const _addUserToDefaultGroups = async (user: User) => {
for (const category of Object.values(DEFAULT_CATEGORIES)) {
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
const groups = await getValues<Group>(

View File

@ -12,13 +12,12 @@ const firestore = admin.firestore()
export const onUpdateUser = functions.firestore
.document('users/{userId}')
.onUpdate(async (change, context) => {
.onUpdate(async (change, _context) => {
const prevUser = change.before.data() as User
const user = change.after.data() as User
const { eventId } = context
if (prevUser.referredByUserId !== user.referredByUserId) {
await handleUserUpdatedReferral(user, eventId)
// await handleUserUpdatedReferral(user, eventId)
}
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
if (!user.referredByUserId) {
console.log(`Not set: referredByUserId ${user.referredByUserId}`)

View File

@ -1,6 +1,7 @@
import { ReactNode } from 'react'
import Head from 'next/head'
import { Challenge } from 'common/challenge'
import { ENV_CONFIG } from 'common/envs/constants'
export type OgCardProps = {
question: string
@ -88,7 +89,7 @@ export function SEO(props: {
{url && (
<meta
property="og:url"
content={'https://manifold.markets' + url}
content={'https://' + ENV_CONFIG.domain + url}
key="url"
/>
)}

View File

@ -4,7 +4,6 @@ import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format'
import { Col } from './layout/col'
import { Spacer } from './layout/spacer'
import { SiteLink } from './site-link'
import { ENV_CONFIG } from 'common/envs/constants'
export function AmountInput(props: {
@ -61,16 +60,7 @@ export function AmountInput(props: {
{error && (
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
{error === 'Insufficient balance' ? (
<>
Not enough funds.
<span className="ml-1 text-indigo-500">
<SiteLink href="/add-funds">Buy more?</SiteLink>
</span>
</>
) : (
error
)}
{error === 'Insufficient balance' ? <>Not enough funds.</> : error}
</div>
)}
</Col>

View File

@ -6,7 +6,7 @@ import clsx from 'clsx'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import { Bet } from 'web/lib/firebase/bets'
import { User } from 'web/lib/firebase/users'
import { getFirstDayProfit, User } from 'web/lib/firebase/users'
import {
formatLargeNumber,
formatMoney,
@ -73,6 +73,7 @@ export function BetsList(props: {
const [sort, setSort] = useState<BetSort>('newest')
const [filter, setFilter] = useState<BetFilter>('open')
const [firstDayProfit, setFirstDayProfit] = useState(0)
const [page, setPage] = useState(0)
const start = page * CONTRACTS_PER_PAGE
const end = start + CONTRACTS_PER_PAGE
@ -84,6 +85,12 @@ export function BetsList(props: {
}
}, [signedInUser, bets, contractsById, getTime])
useEffect(() => {
if (signedInUser && signedInUser.id === user.id) {
getFirstDayProfit(signedInUser.id).then(setFirstDayProfit)
}
}, [signedInUser, user])
if (!bets || !contractsById) {
return <LoadingIndicator />
}
@ -157,14 +164,16 @@ export function BetsList(props: {
(c) => contractsMetrics[c.id].netPayout
)
const totalPnl = user.profitCached.allTime
const totalProfitPercent = (totalPnl / user.totalDeposits) * 100
const totalPnl = (signedInUser?.profitCached.allTime ?? 0) - firstDayProfit
const totalProfitPercent =
(totalPnl / (signedInUser?.totalDeposits ?? 1000)) * 100
const investedProfitPercent =
((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100
return (
<Col className="mt-6">
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
{user.id === signedInUser?.id && (
<Row className="gap-8">
<Col>
<div className="text-sm text-gray-500">Investment value</div>
@ -181,6 +190,7 @@ export function BetsList(props: {
</div>
</Col>
</Row>
)}
<Row className="gap-8">
<select

View File

@ -13,6 +13,8 @@ import { Col } from '../layout/col'
import { Modal } from '../layout/modal'
import { Title } from '../title'
import { InfoTooltip } from '../info-tooltip'
import { useUser } from 'web/hooks/use-user'
import { ENV_CONFIG } from 'common/envs/constants'
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'
@ -20,6 +22,8 @@ export const contractDetailsButtonClassName =
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
const { contract, bets } = props
const user = useUser()
const [open, setOpen] = useState(false)
const formatTime = (dt: number) => dayjs(dt).format('MMM DD, YYYY hh:mm a z')
@ -124,7 +128,9 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
</tbody>
</table>
{contract.mechanism === 'cpmm-1' && !contract.resolution && (
{contract.mechanism === 'cpmm-1' &&
!contract.resolution &&
ENV_CONFIG.whitelistCreators?.includes(user?.username ?? '') && (
<LiquidityPanel contract={contract} />
)}
</Col>

View File

@ -29,7 +29,6 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
import { PaperAirplaneIcon } from '@heroicons/react/outline'
import { track } from 'web/lib/service/analytics'
import { useEvent } from 'web/hooks/use-event'
import { Tipper } from '../tipper'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { useWindowSize } from 'web/hooks/use-window-size'
@ -189,7 +188,6 @@ export function FeedComment(props: {
const {
contract,
comment,
tips,
betsBySameUser,
probAtCreatedTime,
truncate,
@ -282,7 +280,6 @@ export function FeedComment(props: {
shouldTruncate={truncate}
/>
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
<Tipper comment={comment} tips={tips ?? {}} />
{onReplyClick && (
<button
className="font-bold hover:underline"

View File

@ -357,7 +357,6 @@ const GroupMessage = memo(function GroupMessage_(props: {
{formatMoney(sum(Object.values(tips)))}
</span>
)}
{!isCreatorsComment && <Tipper comment={comment} tips={tips} />}
</Row>
</Col>
)

View File

@ -1,18 +1,11 @@
import { SparklesIcon } from '@heroicons/react/solid'
import { Contract } from 'common/contract'
import { Spacer } from './layout/spacer'
import { firebaseLogin } from 'web/lib/firebase/users'
import { ContractsGrid } from './contract/contracts-list'
import { Col } from './layout/col'
import { Row } from './layout/row'
import { withTracking } from 'web/lib/service/analytics'
import { useTracking } from 'web/hooks/use-tracking'
import { SiteLink } from './site-link'
export function LandingPagePanel(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
export function LandingPagePanel() {
useTracking('view landing page')
return (
@ -27,23 +20,28 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
<div className="m-4 max-w-[550px] self-center">
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
<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">
anything!
Tournament
</span>
</div>
</h1>
<Spacer h={6} />
<div className="mb-4 px-2 ">
Create a play-money prediction market on any topic you care about
and bet with your friends on what will happen!
Predict the future and win!
<br />
{/* <br />
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
<SiteLink className="font-semibold" href="/charity">
favorite charity.
<br />
Manifold Markets is partnering with CSPI and the Salem Center of the
University of Texas at Austin to bring a{' '}
<SiteLink
className="underline"
href={
'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting'
}
>
forecasting tournament
</SiteLink>
<br /> */}
.
</div>
</div>
<Spacer h={6} />
@ -54,16 +52,6 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
Get started
</button>{' '}
</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}
/>
</>
)
}

View File

@ -17,16 +17,14 @@ export function ManifoldLogo(props: {
return (
<Link href={user ? '/home' : '/'}>
<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 &&
(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 ? (
<div
className={clsx(

View File

@ -5,30 +5,24 @@ import {
DotsHorizontalIcon,
CashIcon,
HeartIcon,
UserGroupIcon,
TrendingUpIcon,
ChatIcon,
ExternalLinkIcon,
} from '@heroicons/react/outline'
import clsx from 'clsx'
import Link from 'next/link'
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 { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu'
import { ProfileSummary } from './profile-menu'
import NotificationsIcon from 'web/components/notifications-icon'
import React, { useMemo, useState } from 'react'
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import { ENV_CONFIG, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import React from 'react'
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 { Group, GROUP_CHAT_SLUG } from 'common/group'
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'
const logout = async () => {
@ -47,17 +41,31 @@ function getNavigation() {
icon: NotificationsIcon,
},
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
{ name: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
...(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 }]),
]
}
function getMoreNavigation(user?: User | null) {
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) {
@ -110,34 +118,65 @@ function getMoreNavigation(user?: User | null) {
const signedOutNavigation = [
{ name: 'Home', href: '/home', icon: HomeIcon },
{ 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: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
{
name: 'About',
href: 'https://docs.manifold.markets/$how-to',
name: 'Rules',
href: 'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting',
icon: BookOpenIcon,
},
{
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 },
{
name: 'Discord',
href: 'https://discord.gg/eHQBNBqXuh',
icon: ChatIcon,
},
]
const signedInMobileNavigation = [
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
{ name: 'Leaderboard', href: '/leaderboards', icon: TrendingUpIcon },
...(IS_PRIVATE_MANIFOLD
? []
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
? [
{
name: 'Rules',
href: 'https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting',
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() {
@ -232,7 +271,6 @@ export default function Sidebar(props: { className?: string }) {
const currentPage = router.pathname
const user = useUser()
const privateUser = usePrivateUser(user?.id)
// usePing(user?.id)
const navigationOptions = !user ? signedOutNavigation : getNavigation()
@ -240,22 +278,13 @@ export default function Sidebar(props: { className?: string }) {
? signedOutMobileNavigation
: signedInMobileNavigation
const memberItems = (
useMemberGroups(
user?.id,
{ withChatEnabled: true },
{ by: 'mostRecentChatActivityTime' }
) ?? []
).map((group: Group) => ({
name: group.name,
href: `${groupPath(group.slug)}`,
}))
return (
<nav aria-label="Sidebar" className={className}>
<ManifoldLogo className="py-6" twoLine />
{ENV_CONFIG.whitelistCreators?.includes(user?.username ?? '') && (
<CreateQuestionButton user={user} />
)}
<Spacer h={4} />
{user && (
<div className="w-full" style={{ minHeight: 80 }}>
@ -275,17 +304,6 @@ export default function Sidebar(props: { className?: string }) {
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>
{/* Desktop navigation */}
@ -293,85 +311,13 @@ export default function Sidebar(props: { className?: string }) {
{navigationOptions.map((item) => (
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
))}
{user && (
<MenuButton
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>
</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>
</>
)
}

View File

@ -1,9 +1,10 @@
import { ENV_CONFIG } from 'common/envs/constants'
import { InfoBox } from './info-box'
export const PlayMoneyDisclaimer = () => (
<InfoBox
title="Play-money betting"
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!`}
/>
)

View File

@ -39,8 +39,6 @@ import { PortfolioValueSection } from './portfolio/portfolio-value-section'
import { filterDefined } from 'common/util/array'
import { useUserBets } from 'web/hooks/use-user-bets'
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'
export function UserLink(props: {
@ -122,7 +120,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
const yourFollows = useFollows(currentUser?.id)
const isFollowing = yourFollows?.includes(user.id)
const profit = user.profitCached.allTime
const onFollow = () => {
if (!currentUser) return
@ -187,17 +184,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
<Col className="mx-4 -mt-6">
<span className="text-2xl font-bold">{user.name}</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} />
@ -272,25 +258,6 @@ export function UserPage(props: { user: User; currentUser?: User }) {
)}
</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} />
{usersContracts !== 'loading' && contractsById && usersComments ? (
@ -322,6 +289,12 @@ export function UserPage(props: { user: User; currentUser?: User }) {
</span>
),
},
...(isCurrentUser ||
[
...(ENV_CONFIG.whitelistCreators ?? []),
'JamesGrugett',
].includes(currentUser?.username ?? '')
? [
{
title: 'Bets',
content: (
@ -337,8 +310,12 @@ export function UserPage(props: { user: User; currentUser?: User }) {
/>
</div>
),
tabIcon: <span className="px-0.5 font-bold">{betCount}</span>,
tabIcon: (
<span className="px-0.5 font-bold">{betCount}</span>
),
},
]
: []),
]}
/>
) : (

View File

@ -1,4 +1,4 @@
import { defaults, debounce } from 'lodash'
import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
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?: {
defaultSort?: Sort
shouldLoadFromStorage?: boolean

View File

@ -213,14 +213,15 @@ export async function listAllUsers() {
return docs.map((doc) => doc.data())
}
export function getTopTraders(period: Period) {
export async function getTopTraders(period: Period) {
const topTraders = query(
users,
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) {
@ -236,6 +237,20 @@ export async function getTopFollowed() {
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(
users,
orderBy('followerCountCached', 'desc'),

View File

@ -8,8 +8,8 @@
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"",
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev",
"dev:dev": "yarn devdev",
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE yarn dev",
"dev:local": "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8080 yarn devdev",
"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: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",
"build": "next build",
"start": "next start",

View File

@ -20,7 +20,6 @@ import { Bet, listAllBets } from 'web/lib/firebase/bets'
import { Comment, listAllComments } from 'web/lib/firebase/comments'
import Custom404 from '../404'
import { AnswersPanel } from 'web/components/answers/answers-panel'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { Leaderboard } from 'web/components/leaderboard'
import { resolvedPayout } from 'common/calculate'
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 { FeedBet } from 'web/components/feed/feed-bets'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
export async function getStaticProps(props: {
params: { username: string; contractSlug: string }
}) {
const { username, contractSlug } = props.params
@ -84,14 +82,6 @@ export default function ContractPage(props: {
slug: string
backToHome?: () => void
}) {
props = usePropz(props, getStaticPropz) ?? {
contract: null,
username: '',
comments: [],
bets: [],
slug: '',
}
const inIframe = useIsIframe()
if (inIframe) {
return <ContractEmbedPage {...props} />

View File

@ -13,6 +13,8 @@ export async function getStaticPropz(props: { params: { username: string } }) {
const { username } = props.params
const user = await getUserByUsername(username)
if (user) user.profitCached.allTime = 0
return {
props: {
user,
@ -38,7 +40,7 @@ export default function UserProfile(props: { user: User | null }) {
useTracking('view user profile', { username })
if (user === undefined) return <div />
if (user === undefined || user?.username !== username) return <div />
return user ? (
<UserPage user={user} currentUser={currentUser || undefined} />

View File

@ -10,6 +10,8 @@ import { mapKeys } from 'lodash'
import { useAdmin } from 'web/hooks/use-admin'
import { contractPath } from 'web/lib/firebase/contracts'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { useEffect, useState } from 'react'
import { getFirstDayProfit } from 'web/lib/firebase/users'
export const getServerSideProps = redirectIfLoggedOut('/')
@ -27,17 +29,50 @@ function UsersTable() {
// Map private users by user 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
const fullUsers = users
.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)
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 url = URL.createObjectURL(blob)
const a = document.createElement('a')
@ -90,6 +125,11 @@ function UsersTable() {
name: 'Balance',
formatter: (cell) => (cell as number).toFixed(0),
},
{
id: 'profit',
name: 'profit',
formatter: (cell) => (cell as number).toFixed(0),
},
{
id: 'id',
name: 'ID',

View File

@ -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.' })
}
}

View File

@ -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.' })
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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.' })
}
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -33,10 +33,10 @@ export default function CharityPageWrapper() {
if (!charity) {
return <Custom404 />
}
return <CharityPage charity={charity} />
// return <CharityPage charity={charity} />
}
function CharityPage(props: { charity: Charity }) {
function _CharityPage(props: { charity: Charity }) {
const { charity } = props
const { name, photo, description } = charity

View File

@ -20,9 +20,7 @@ import { quadraticMatches } from 'common/quadratic-funding'
import { Txn } from 'common/txn'
import { useTracking } from 'web/hooks/use-tracking'
import { searchInAny } from 'common/util/parse'
import { getUser } from 'web/lib/firebase/users'
import { SiteLink } from 'web/components/site-link'
import { User } from 'common/user'
import { SEO } from 'web/components/SEO'
export async function getStaticProps() {
@ -37,7 +35,6 @@ export async function getStaticProps() {
])
const matches = quadraticMatches(txns, totalRaised)
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
const mostRecentDonor = await getUser(txns[txns.length - 1].fromId)
return {
props: {
@ -46,7 +43,6 @@ export async function getStaticProps() {
matches,
txns,
numDonors,
mostRecentDonor,
},
revalidate: 60,
}
@ -90,9 +86,8 @@ export default function Charity(props: {
matches: { [charityId: string]: number }
txns: Txn[]
numDonors: number
mostRecentDonor: User
}) {
const { totalRaised, charities, matches, numDonors, mostRecentDonor } = props
const { totalRaised, charities, matches, numDonors } = props
const [query, setQuery] = useState('')
const debouncedQuery = debounce(setQuery, 50)
@ -149,8 +144,8 @@ export default function Charity(props: {
},
{
name: 'Most recent donor',
stat: mostRecentDonor.name ?? 'Nobody',
url: `/${mostRecentDonor.username}`,
stat: 'Nobody',
url: `/`,
},
]}
/>

View File

@ -1,13 +1,12 @@
import { Answer } from 'common/answer'
import { searchInAny } from 'common/util/parse'
import { sortBy } from 'lodash'
import { useState } from 'react'
import { ContractsGrid } from 'web/components/contract/contracts-list'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { useContracts } from 'web/hooks/use-contracts'
import {
Sort,
useInitialQueryAndSort,
useQueryAndSortParams,
} from 'web/hooks/use-sort-and-query-params'
const MAX_CONTRACTS_RENDERED = 100
@ -27,9 +26,8 @@ export default function ContractSearchFirestore(props: {
const contracts = useContracts()
const { querySortOptions, additionalFilter } = props
const { initialSort, initialQuery } = useInitialQueryAndSort(querySortOptions)
const [sort, setSort] = useState(initialSort || 'newest')
const [query, setQuery] = useState(initialQuery)
const { query, setQuery, sort, setSort } =
useQueryAndSortParams(querySortOptions)
let matches = (contracts ?? []).filter((c) =>
searchInAny(
@ -49,11 +47,7 @@ export default function ContractSearchFirestore(props: {
matches.sort((a, b) => a.createdTime - b.createdTime)
} else if (sort === 'close-date') {
matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
matches = sortBy(
matches,
(contract) =>
(sort === 'close-date' ? -1 : 1) * (contract.closeTime ?? Infinity)
)
matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity)
} else if (sort === 'most-traded') {
matches.sort((a, b) => b.volume - a.volume)
} else if (sort === 'score') {
@ -110,9 +104,8 @@ export default function ContractSearchFirestore(props: {
value={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="newest">Newest</option>
<option value="most-traded">Most traded</option>
<option value="24-hour-vol">24h volume</option>
<option value="close-date">Closing soon</option>

View File

@ -28,6 +28,7 @@ import { GroupSelector } from 'web/components/groups/group-selector'
import { User } from 'common/user'
import { TextEditor, useTextEditor } from 'web/components/editor'
import { Checkbox } from 'web/components/checkbox'
import { ENV_CONFIG } from 'common/envs/constants'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { Title } from 'web/components/title'
import { SEO } from 'web/components/SEO'
@ -61,6 +62,16 @@ export default function Create() {
}, [params.q])
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 />
return (
@ -111,7 +122,8 @@ export function NewContract(props: {
const [outcomeType, setOutcomeType] = useState<outcomeType>(
(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 [maxString, setMaxString] = useState(params?.max ?? '')
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
@ -283,6 +295,56 @@ export function NewContract(props: {
<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' && (
<MultipleChoiceAnswers setAnswers={setAnswers} />
)}
@ -448,12 +510,6 @@ export function NewContract(props: {
{ante > balance && (
<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>
<button
className="btn btn-xs btn-primary"
onClick={() => (window.location.href = '/add-funds')}
>
Get M$
</button>
</div>
)}
</div>

View File

@ -160,7 +160,7 @@ export default function GroupPage(props: {
const privateUser = usePrivateUser(user?.id)
useSaveReferral(user, {
defaultReferrerUsername: creator.username,
defaultReferrerUsername: creator?.username,
groupId: group?.id,
})

View File

@ -1,6 +1,4 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { PlusSmIcon } from '@heroicons/react/solid'
import { Page } from 'web/components/page'
import { Col } from 'web/components/layout/col'
@ -10,7 +8,6 @@ import { Contract } from 'common/contract'
import { ContractPageContent } from './[username]/[contractSlug]'
import { getContractFromSlug } from 'web/lib/firebase/contracts'
import { useTracking } from 'web/hooks/use-tracking'
import { track } from 'web/lib/service/analytics'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { useSaveReferral } from 'web/hooks/use-save-referral'
@ -19,7 +16,6 @@ export const getServerSideProps = redirectIfLoggedOut('/')
const Home = () => {
const [contract, setContract] = useContractPage()
const router = useRouter()
useTracking('view home')
useSaveReferral()
@ -41,16 +37,6 @@ const Home = () => {
}}
/>
</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>
{contract && (

View File

@ -6,6 +6,9 @@ import { ManifoldLogo } from 'web/components/nav/manifold-logo'
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
import { useSaveReferral } from 'web/hooks/use-save-referral'
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 (_) => {
// 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 } }
})
export default function Home(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
export default function Home(_props: { hotContracts: Contract[] }) {
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 (
<Page>
<SEO
@ -41,7 +51,7 @@ export default function Home(props: { hotContracts: Contract[] }) {
</div>
<Col className="items-center">
<Col className="max-w-3xl">
<LandingPagePanel hotContracts={hotContracts ?? []} />
<LandingPagePanel />
{/* <p className="mt-6 text-gray-500">
View{' '}
<SiteLink href="/markets" className="font-bold text-gray-700">

View File

@ -1,19 +1,11 @@
import { Col } from 'web/components/layout/col'
import { Leaderboard } from 'web/components/leaderboard'
import { Page } from 'web/components/page'
import {
getTopCreators,
getTopTraders,
getTopFollowed,
Period,
User,
} from 'web/lib/firebase/users'
import { formatMoney } from 'common/util/format'
import { User, getFirstDayProfit, listAllUsers } from 'web/lib/firebase/users'
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 { SEO } from 'web/components/SEO'
import { sortBy } from 'lodash'
export async function getStaticProps() {
const props = await fetchProps()
@ -25,134 +17,69 @@ export async function getStaticProps() {
}
const fetchProps = async () => {
const [allTime, monthly, weekly, daily] = await Promise.all([
queryLeaderboardUsers('allTime'),
queryLeaderboardUsers('monthly'),
queryLeaderboardUsers('weekly'),
queryLeaderboardUsers('daily'),
])
const topFollowed = await getTopFollowed()
const users = await listAllUsers()
const firstDayProfit = await Promise.all(
users.map((user) => getFirstDayProfit(user.id))
)
const userProfit = users.map(
(user, i) => [user, user.profitCached.allTime - firstDayProfit[i]] as const
)
const topTradersProfit = sortBy(userProfit, ([_, profit]) => profit)
.reverse()
.filter(
([user]) =>
user.username !== 'SalemCenter' &&
user.username !== 'RichardHanania' &&
user.username !== 'JamesGrugett'
)
.slice(0, 20)
return {
allTime,
monthly,
weekly,
daily,
topFollowed,
}
}
const topTraders = topTradersProfit.map(([user]) => user)
// Hide profit for now.
topTraders.forEach((user) => {
user.profitCached.allTime = 0
})
const queryLeaderboardUsers = async (period: Period) => {
const [topTraders, topCreators] = await Promise.all([
getTopTraders(period),
getTopCreators(period),
])
return {
topTraders,
topCreators,
}
}
type leaderboard = {
topTraders: User[]
topCreators: User[]
}
export default function Leaderboards(_props: { topTraders: User[] }) {
const [{ topTraders }, setProps] =
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(() => {
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')
return (
<Page>
<SEO
title="Leaderboards"
description="Manifold's leaderboards show the top traders and market creators."
title="Leaderboard"
description="See the top traders of the CSPI/Salem Center Tournament."
url="/leaderboards"
/>
<Title text={'Leaderboards'} className={'hidden md:block'} />
<Tabs
currentPageForAnalytics={'leaderboards'}
defaultIndex={1}
tabs={[
{
title: 'All Time',
content: LeaderboardWithPeriod('allTime'),
},
// TODO: Enable this near the end of July!
<Col className="mx-4 max-w-sm items-center gap-10 lg:flex-row">
<Leaderboard
className="my-4"
title="🏅 Top traders"
users={topTraders}
columns={
[
// Hide profit for now.
// {
// title: 'Monthly',
// content: LeaderboardWithPeriod('monthly'),
// header: 'Total profit',
// renderCell: (user) => formatMoney(user.profitCached[period]),
// },
{
title: 'Weekly',
content: LeaderboardWithPeriod('weekly'),
},
{
title: 'Daily',
content: LeaderboardWithPeriod('daily'),
},
]}
]
}
/>
</Col>
</Page>
)
}

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'
import { RefreshIcon } from '@heroicons/react/outline'
import { AddFundsButton } from 'web/components/add-funds-button'
import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title'
@ -240,7 +239,6 @@ export default function ProfilePage() {
<label className="label">Balance</label>
<Row className="ml-1 items-start gap-4 text-gray-500">
{formatMoney(user?.balance || 0)}
<AddFundsButton />
</Row>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View 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