diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx
new file mode 100644
index 00000000..558fc5f6
--- /dev/null
+++ b/web/components/portfolio/portfolio-value-graph.tsx
@@ -0,0 +1,83 @@
+import { ResponsiveLine } from '@nivo/line'
+import { PortfolioMetrics } from 'common/user'
+import { formatMoney } from 'common/util/format'
+import { DAY_MS } from 'common/util/time'
+import { last } from 'lodash'
+import { memo } from 'react'
+import { useWindowSize } from 'web/hooks/use-window-size'
+import { formatTime } from 'web/lib/util/time'
+
+export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
+ portfolioHistory: PortfolioMetrics[]
+ height?: number
+ period?: string
+}) {
+ const { portfolioHistory, height, period } = props
+
+ const { width } = useWindowSize()
+
+ const portfolioHistoryFiltered = portfolioHistory.filter((p) => {
+ switch (period) {
+ case 'daily':
+ return p.timestamp > Date.now() - 1 * DAY_MS
+ case 'weekly':
+ return p.timestamp > Date.now() - 7 * DAY_MS
+ case 'monthly':
+ return p.timestamp > Date.now() - 30 * DAY_MS
+ case 'allTime':
+ return true
+ default:
+ return true
+ }
+ })
+
+ const points = portfolioHistoryFiltered.map((p) => {
+ return {
+ x: new Date(p.timestamp),
+ y: p.balance + p.investmentValue,
+ }
+ })
+ const data = [{ id: 'Value', data: points, color: '#11b981' }]
+ const numXTickValues = !width || width < 800 ? 2 : 5
+ const numYTickValues = 4
+ const endDate = last(points)?.x
+ const includeTime = period === 'daily'
+ return (
+
= 800 ? 350 : 250) }}
+ >
+ p.y)),
+ }}
+ gridYValues={numYTickValues}
+ curve="monotoneX"
+ colors={{ datum: 'color' }}
+ axisBottom={{
+ tickValues: numXTickValues,
+ format: (time) => formatTime(+time, includeTime),
+ }}
+ pointBorderColor="#fff"
+ pointSize={points.length > 100 ? 0 : 6}
+ axisLeft={{
+ tickValues: numYTickValues,
+ format: (value) => formatMoney(value),
+ }}
+ enableGridX={!!width && width >= 800}
+ enableGridY={true}
+ enableSlices="x"
+ animate={false}
+ >
+
+ )
+})
diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx
new file mode 100644
index 00000000..a992e87e
--- /dev/null
+++ b/web/components/portfolio/portfolio-value-section.tsx
@@ -0,0 +1,47 @@
+import { PortfolioMetrics } from 'common/user'
+import { formatMoney } from 'common/util/format'
+import { last } from 'lodash'
+import { memo, useState } from 'react'
+import { Period } from 'web/lib/firebase/users'
+import { Col } from '../layout/col'
+import { Row } from '../layout/row'
+import { PortfolioValueGraph } from './portfolio-value-graph'
+
+export const PortfolioValueSection = memo(
+ function PortfolioValueSection(props: {
+ portfolioHistory: PortfolioMetrics[]
+ }) {
+ const { portfolioHistory } = props
+ const lastPortfolioMetrics = last(portfolioHistory)
+ const [portfolioPeriod] = useState('allTime')
+
+ if (portfolioHistory.length === 0 || !lastPortfolioMetrics) {
+ return No portfolio history data yet
+ }
+
+ return (
+
+
+
+
+
Portfolio value
+
+ {formatMoney(
+ lastPortfolioMetrics.balance +
+ lastPortfolioMetrics.investmentValue
+ )}
+
+
+
+ {
+ //TODO: enable day/week/monthly as data becomes available
+ }
+
+
+
+ )
+ }
+)
diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx
index cd896c59..2019a9de 100644
--- a/web/components/user-page.tsx
+++ b/web/components/user-page.tsx
@@ -6,7 +6,12 @@ import { LinkIcon } from '@heroicons/react/solid'
import { PencilIcon } from '@heroicons/react/outline'
import Confetti from 'react-confetti'
-import { follow, unfollow, User } from 'web/lib/firebase/users'
+import {
+ follow,
+ unfollow,
+ User,
+ getPortfolioHistory,
+} from 'web/lib/firebase/users'
import { CreatorContractsList } from './contract/contracts-list'
import { SEO } from './SEO'
import { Page } from './page'
@@ -30,6 +35,7 @@ import { getUserBets } from 'web/lib/firebase/bets'
import { FollowersButton, FollowingButton } from './following-button'
import { useFollows } from 'web/hooks/use-follows'
import { FollowButton } from './follow-button'
+import { PortfolioMetrics } from 'common/user'
export function UserLink(props: {
name: string
@@ -67,6 +73,7 @@ export function UserPage(props: {
'loading'
)
const [usersBets, setUsersBets] = useState('loading')
+ const [, setUsersPortfolioHistory] = useState([])
const [commentsByContract, setCommentsByContract] = useState<
Map | 'loading'
>('loading')
@@ -83,6 +90,7 @@ export function UserPage(props: {
getUsersComments(user.id).then(setUsersComments)
listContracts(user.id).then(setUsersContracts)
getUserBets(user.id, { includeRedemptions: false }).then(setUsersBets)
+ getPortfolioHistory(user.id).then(setUsersPortfolioHistory)
}, [user])
// TODO: display comments on groups
@@ -243,6 +251,7 @@ export function UserPage(props: {
+
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
+ {
+ // TODO: add portfolio-value-section here
+ }
(
+ query(
+ collectionGroup(db, 'portfolioHistory'),
+ where('userId', '==', userId),
+ orderBy('timestamp', 'asc')
+ )
+ )
+}
+
export function listenForFollows(
userId: string,
setFollowIds: (followIds: string[]) => void
diff --git a/web/lib/util/time.ts b/web/lib/util/time.ts
index a5844b34..4bd76b91 100644
--- a/web/lib/util/time.ts
+++ b/web/lib/util/time.ts
@@ -5,3 +5,13 @@ dayjs.extend(relativeTime)
export function fromNow(time: number) {
return dayjs(time).fromNow()
}
+
+export function formatTime(time: number, includeTime: boolean) {
+ const d = dayjs(time)
+
+ if (d.isSame(Date.now(), 'day')) return d.format('ha')
+
+ if (includeTime) return dayjs(time).format('MMM D, ha')
+
+ return dayjs(time).format('MMM D')
+}
diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx
index ed05413a..7e141e72 100644
--- a/web/pages/leaderboards.tsx
+++ b/web/pages/leaderboards.tsx
@@ -4,8 +4,8 @@ import { Page } from 'web/components/page'
import {
getTopCreators,
getTopTraders,
- LeaderboardPeriod,
getTopFollowed,
+ Period,
User,
} from 'web/lib/firebase/users'
import { formatMoney } from 'common/util/format'
@@ -20,7 +20,7 @@ export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
return queryLeaderboardUsers('allTime')
}
-const queryLeaderboardUsers = async (period: LeaderboardPeriod) => {
+const queryLeaderboardUsers = async (period: Period) => {
const [topTraders, topCreators, topFollowed] = await Promise.all([
getTopTraders(period).catch(() => {}),
getTopCreators(period).catch(() => {}),
@@ -50,7 +50,7 @@ export default function Leaderboards(props: {
const [topTradersState, setTopTraders] = useState(props.topTraders)
const [topCreatorsState, setTopCreators] = useState(props.topCreators)
const [isLoading, setLoading] = useState(false)
- const [period, setPeriod] = useState('allTime')
+ const [period, setPeriod] = useState('allTime')
useEffect(() => {
setLoading(true)
@@ -61,7 +61,7 @@ export default function Leaderboards(props: {
})
}, [period])
- const LeaderboardWithPeriod = (period: LeaderboardPeriod) => {
+ const LeaderboardWithPeriod = (period: Period) => {
return (
<>
@@ -127,7 +127,7 @@ export default function Leaderboards(props: {
defaultIndex={0}
onClick={(title, index) => {
const period = ['allTime', 'monthly', 'weekly', 'daily'][index]
- setPeriod(period as LeaderboardPeriod)
+ setPeriod(period as Period)
}}
tabs={[
{