diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 74480de5..dbb2db56 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -77,7 +77,7 @@ export function BetsList(props: { user: User }) { }, [contractList]) const [sort, setSort] = useState('newest') - const [filter, setFilter] = useState('open') + const [filter, setFilter] = useState('all') const [page, setPage] = useState(0) const start = page * CONTRACTS_PER_PAGE const end = start + CONTRACTS_PER_PAGE @@ -155,34 +155,25 @@ export function BetsList(props: { user: User }) { (c) => contractsMetrics[c.id].netPayout ) - const totalPnl = user.profitCached.allTime - const totalProfitPercent = (totalPnl / user.totalDeposits) * 100 const investedProfitPercent = ((currentBetsValue - currentInvested) / (currentInvested + 0.1)) * 100 return ( - - - -
Investment value
-
- {formatMoney(currentNetInvestment)}{' '} - -
- - -
Total profit
-
- {formatMoney(totalPnl)}{' '} - -
- -
+ + +
+ Investment value +
+
+ {formatMoney(currentNetInvestment)}{' '} + +
+ - + - + {displayedContracts.length === 0 ? ( diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 331dcb80..ba589d0e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -103,6 +103,7 @@ export function ContractSearch(props: { loadMore: () => void ) => ReactNode autoFocus?: boolean + profile?: boolean | undefined }) { const { user, @@ -123,6 +124,7 @@ export function ContractSearch(props: { maxResults, renderContracts, autoFocus, + profile, } = props const [state, setState] = usePersistentState( @@ -239,6 +241,10 @@ export function ContractSearch(props: { /> {renderContracts ? ( renderContracts(renderedContracts, performQuery) + ) : renderedContracts && renderedContracts.length === 0 && profile ? ( +

+ This creator does not yet have any markets. +

) : ( ) } diff --git a/web/components/following-button.tsx b/web/components/following-button.tsx index 135f43a8..c897c89b 100644 --- a/web/components/following-button.tsx +++ b/web/components/following-button.tsx @@ -13,16 +13,18 @@ import { useDiscoverUsers } from 'web/hooks/use-users' import { TextButton } from './text-button' import { track } from 'web/lib/service/analytics' -export function FollowingButton(props: { user: User }) { - const { user } = props +export function FollowingButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const followingIds = useFollows(user.id) const followerIds = useFollowers(user.id) return ( <> - setIsOpen(true)}> - {followingIds?.length ?? ''}{' '} + setIsOpen(true)} className={className}> + + {followingIds?.length ?? ''} + {' '} Following @@ -69,15 +71,15 @@ export function EditFollowingButton(props: { user: User; className?: string }) { ) } -export function FollowersButton(props: { user: User }) { - const { user } = props +export function FollowersButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const followingIds = useFollows(user.id) const followerIds = useFollowers(user.id) return ( <> - setIsOpen(true)}> + setIsOpen(true)} className={className}> {followerIds?.length ?? ''}{' '} Followers diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index e6271466..5c9d2edd 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -14,14 +14,14 @@ import { firebaseLogin } from 'web/lib/firebase/users' import { GroupLinkItem } from 'web/pages/groups' import toast from 'react-hot-toast' -export function GroupsButton(props: { user: User }) { - const { user } = props +export function GroupsButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const groups = useMemberGroups(user.id) return ( <> - setIsOpen(true)}> + setIsOpen(true)} className={className}> {groups?.length ?? ''} Groups diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 980a3cfc..b82131ec 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx' import { useRouter, NextRouter } from 'next/router' import { ReactNode, useState } from 'react' import { track } from '@amplitude/analytics-browser' +import { Col } from './col' type Tab = { title: string @@ -55,11 +56,13 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) { )} aria-current={activeIndex === i ? 'page' : undefined} > - {tab.tabIcon && {tab.tabIcon}} {tab.badge ? ( {tab.badge} ) : null} - {tab.title} + + {tab.tabIcon &&
{tab.tabIcon}
} + {tab.title} + ))} diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index d8489b47..6ed5d195 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -1,72 +1,155 @@ import { ResponsiveLine } from '@nivo/line' import { PortfolioMetrics } from 'common/user' +import { filterDefined } from 'common/util/array' import { formatMoney } from 'common/util/format' +import dayjs from 'dayjs' import { last } from 'lodash' import { memo } from 'react' import { useWindowSize } from 'web/hooks/use-window-size' -import { formatTime } from 'web/lib/util/time' +import { Col } from '../layout/col' export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { portfolioHistory: PortfolioMetrics[] mode: 'value' | 'profit' + handleGraphDisplayChange: (arg0: string | number | null) => void height?: number - includeTime?: boolean }) { - const { portfolioHistory, height, includeTime, mode } = props + const { portfolioHistory, height, mode, handleGraphDisplayChange } = props const { width } = useWindowSize() - const points = portfolioHistory.map((p) => { - const { timestamp, balance, investmentValue, totalDeposits } = p - const value = balance + investmentValue - const profit = value - totalDeposits + const valuePoints = getPoints('value', portfolioHistory) + const posProfitPoints = getPoints('posProfit', portfolioHistory) + const negProfitPoints = getPoints('negProfit', portfolioHistory) + + const valuePointsY = valuePoints.map((p) => p.y) + const posProfitPointsY = posProfitPoints.map((p) => p.y) + const negProfitPointsY = negProfitPoints.map((p) => p.y) + + let data + + if (mode === 'value') { + data = [{ id: 'value', data: valuePoints, color: '#4f46e5' }] + } else { + data = [ + { + id: 'negProfit', + data: negProfitPoints, + color: '#dc2626', + }, + { + id: 'posProfit', + data: posProfitPoints, + color: '#14b8a6', + }, + ] + } + const numYTickValues = 2 + const endDate = last(data[0].data)?.x + + const yMin = + mode === 'value' + ? Math.min(...filterDefined(valuePointsY)) + : Math.min( + ...filterDefined(negProfitPointsY), + ...filterDefined(posProfitPointsY) + ) + + const yMax = + mode === 'value' + ? Math.max(...filterDefined(valuePointsY)) + : Math.max( + ...filterDefined(negProfitPointsY), + ...filterDefined(posProfitPointsY) + ) - return { - x: new Date(timestamp), - y: mode === 'value' ? value : profit, - } - }) - const data = [{ id: 'Value', data: points, color: '#11b981' }] - const numXTickValues = !width || width < 800 ? 2 : 5 - const numYTickValues = 4 - const endDate = last(points)?.x return (
= 800 ? 350 : 250) }} + style={{ height: height ?? (!width || width >= 800 ? 200 : 100) }} + onMouseLeave={() => handleGraphDisplayChange(null)} > p.y)), + min: yMin, + max: yMax, }} - gridYValues={numYTickValues} curve="stepAfter" enablePoints={false} colors={{ datum: 'color' }} axisBottom={{ - tickValues: numXTickValues, - format: (time) => formatTime(+time, !!includeTime), + tickValues: 0, }} pointBorderColor="#fff" - pointSize={points.length > 100 ? 0 : 6} + pointSize={valuePoints.length > 100 ? 0 : 6} axisLeft={{ tickValues: numYTickValues, - format: (value) => formatMoney(value), + format: '.3s', }} - enableGridX={!!width && width >= 800} + enableGridX={false} enableGridY={true} + gridYValues={numYTickValues} enableSlices="x" animate={false} yFormat={(value) => formatMoney(+value)} + enableArea={true} + areaOpacity={0.1} + sliceTooltip={({ slice }) => { + handleGraphDisplayChange(slice.points[0].data.yFormatted) + return ( +
+
+ +
+ {dayjs(slice.points[0].data.xFormatted).format('MMM/D/YY')} +
+
+ {dayjs(slice.points[0].data.xFormatted).format('h:mm A')} +
+ +
+ {/* ))} */} +
+ ) + }} >
) }) + +export function getPoints( + line: 'value' | 'posProfit' | 'negProfit', + portfolioHistory: PortfolioMetrics[] +) { + const points = portfolioHistory.map((p) => { + const { timestamp, balance, investmentValue, totalDeposits } = p + const value = balance + investmentValue + + const profit = value - totalDeposits + let posProfit = null + let negProfit = null + if (profit < 0) { + negProfit = profit + } else { + posProfit = profit + } + + return { + x: new Date(timestamp), + y: + line === 'value' ? value : line === 'posProfit' ? posProfit : negProfit, + } + }) + return points +} diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index a0006c60..ec364c8d 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -1,11 +1,12 @@ +import clsx from 'clsx' import { formatMoney } from 'common/util/format' import { last } from 'lodash' import { memo, useRef, useState } from 'react' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { Period } from 'web/lib/firebase/users' +import { PillButton } from '../buttons/pill-button' import { Col } from '../layout/col' import { Row } from '../layout/row' -import { Spacer } from '../layout/spacer' import { PortfolioValueGraph } from './portfolio-value-graph' export const PortfolioValueSection = memo( @@ -14,6 +15,13 @@ export const PortfolioValueSection = memo( const [portfolioPeriod, setPortfolioPeriod] = useState('weekly') const portfolioHistory = usePortfolioHistory(userId, portfolioPeriod) + const [graphMode, setGraphMode] = useState<'profit' | 'value'>('value') + const [graphDisplayNumber, setGraphDisplayNumber] = useState< + number | string | null + >(null) + const handleGraphDisplayChange = (num: string | number | null) => { + setGraphDisplayNumber(num) + } // Remember the last defined portfolio history. const portfolioRef = useRef(portfolioHistory) @@ -28,43 +36,144 @@ export const PortfolioValueSection = memo( const { balance, investmentValue, totalDeposits } = lastPortfolioMetrics const totalValue = balance + investmentValue const totalProfit = totalValue - totalDeposits - return ( <> - - -
Profit
-
{formatMoney(totalProfit)}
- - + + + setGraphMode('value')} + > +
+ Portfolio value +
+
+ {graphMode === 'value' + ? graphDisplayNumber + ? graphDisplayNumber + : formatMoney(totalValue) + : formatMoney(totalValue)} +
+ + setGraphMode('profit')} + > +
Profit
+
0 + ? 'text-teal-500' + : 'text-red-600' + : totalProfit > 0 + ? 'text-teal-500' + : 'text-red-600', + 'text-lg sm:text-xl' + )} + > + {graphMode === 'profit' + ? graphDisplayNumber + ? graphDisplayNumber + : formatMoney(totalProfit) + : formatMoney(totalProfit)} +
+ +
- - -
Portfolio value
-
{formatMoney(totalValue)}
- - ) } ) + +export function PortfolioPeriodSelection(props: { + setPortfolioPeriod: (string: any) => void + portfolioPeriod: string + className?: string + selectClassName?: string +}) { + const { setPortfolioPeriod, portfolioPeriod, className, selectClassName } = + props + return ( + + + + + + + ) +} + +export function GraphToggle(props: { + setGraphMode: (mode: 'profit' | 'value') => void + graphMode: string +}) { + const { setGraphMode, graphMode } = props + return ( + + { + setGraphMode('value') + }} + xs={true} + className="z-50" + > + Value + + { + setGraphMode('profit') + }} + xs={true} + className="z-50" + > + Profit + + + ) +} diff --git a/web/components/profile/user-likes-button.tsx b/web/components/profile/user-likes-button.tsx index 3d4fa9ac..666036a8 100644 --- a/web/components/profile/user-likes-button.tsx +++ b/web/components/profile/user-likes-button.tsx @@ -10,15 +10,15 @@ import { XIcon } from '@heroicons/react/outline' import { unLikeContract } from 'web/lib/firebase/likes' import { contractPath } from 'web/lib/firebase/contracts' -export function UserLikesButton(props: { user: User }) { - const { user } = props +export function UserLikesButton(props: { user: User; className?: string }) { + const { user, className } = props const [isOpen, setIsOpen] = useState(false) const likedContracts = useUserLikedContracts(user.id) return ( <> - setIsOpen(true)}> + setIsOpen(true)} className={className}> {likedContracts?.length ?? ''}{' '} Likes diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index b164e10c..9a548031 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -13,14 +13,18 @@ import { getUser, updateUser } from 'web/lib/firebase/users' import { TextButton } from 'web/components/text-button' import { UserLink } from 'web/components/user-link' -export function ReferralsButton(props: { user: User; currentUser?: User }) { - const { user, currentUser } = props +export function ReferralsButton(props: { + user: User + currentUser?: User + className?: string +}) { + const { user, currentUser, className } = props const [isOpen, setIsOpen] = useState(false) const referralIds = useReferrals(user.id) return ( <> - setIsOpen(true)}> + setIsOpen(true)} className={className}> {referralIds?.length ?? ''}{' '} Referrals diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index f9845fbe..bcbb395e 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -1,8 +1,13 @@ import clsx from 'clsx' import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' +import { NextRouter, useRouter } from 'next/router' import { LinkIcon } from '@heroicons/react/solid' -import { PencilIcon } from '@heroicons/react/outline' +import { + ChatIcon, + FolderIcon, + PencilIcon, + ScaleIcon, +} from '@heroicons/react/outline' import { User } from 'web/lib/firebase/users' import { useUser } from 'web/hooks/use-user' @@ -24,39 +29,23 @@ import { FollowersButton, FollowingButton } from './following-button' import { UserFollowButton } from './follow-button' import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' -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 { BettingStreakModal, hasCompletedStreakToday, } from 'web/components/profile/betting-streak-modal' -import { REFERRAL_AMOUNT } from 'common/economy' import { LoansModal } from './profile/loans-modal' -import { UserLikesButton } from 'web/components/profile/user-likes-button' -import { PAST_BETS } from 'common/user' -import { capitalize } from 'lodash' export function UserPage(props: { user: User }) { const { user } = props const router = useRouter() const currentUser = useUser() const isCurrentUser = user.id === currentUser?.id - const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) const [showConfetti, setShowConfetti] = useState(false) - const [showBettingStreakModal, setShowBettingStreakModal] = useState(false) - const [showLoansModal, setShowLoansModal] = useState(false) useEffect(() => { const claimedMana = router.query['claimed-mana'] === 'yes' - const showBettingStreak = router.query['show'] === 'betting-streak' - setShowBettingStreakModal(showBettingStreak) - setShowConfetti(claimedMana || showBettingStreak) - - const showLoansModel = router.query['show'] === 'loans' - setShowLoansModal(showLoansModel) - + setShowConfetti(claimedMana) const query = { ...router.query } if (query.claimedMana || query.show) { delete query['claimed-mana'] @@ -85,218 +74,159 @@ export function UserPage(props: { user: User }) { {showConfetti && ( )} - - {showLoansModal && ( - - )} - {/* Banner image up top, with an circle avatar overlaid */} -
-
-
+ + -
- - {/* Top right buttons (e.g. edit, follow) */} -
- {!isCurrentUser && } {isCurrentUser && ( - - {' '} -
Edit
-
+
+ + {' '} + +
)} -
-
- {/* Profile details: name, username, bio, and link to twitter/discord */} - - - - - {user.name} - - @{user.username} - - - - - = 0 ? 'text-green-600' : 'text-red-400' - )} - > - {formatMoney(profit)} + +
+ + + {user.name} - profit - - setShowBettingStreakModal(true)} - > - 🔥 {user.currentBettingStreak ?? 0} - streak - - setShowLoansModal(true)} - > - - 🏦 {formatMoney(user.nextLoanCached ?? 0)} + + @{user.username} - next loan - + {isCurrentUser && ( + + )} + {!isCurrentUser && } +
+
- - {user.bio && ( - <> -
- -
- - - )} - {(user.website || user.twitterHandle || user.discordHandle) && ( - - {user.website && ( - - - - {user.website} - - - )} - - {user.twitterHandle && ( - - - Twitter - - {user.twitterHandle} - - - - )} - - {user.discordHandle && ( - - - Discord - - {user.discordHandle} - - - - )} - - )} - {currentUser?.id === user.id && REFERRAL_AMOUNT > 0 && ( - - - - Earn {formatMoney(REFERRAL_AMOUNT)} when you refer a friend! - {' '} - You've gotten{' '} - - - - - )} - - ), - }, - { - title: 'Comments', - content: ( - - - - ), - }, - { - title: capitalize(PAST_BETS), - content: ( - <> - - - ), - }, - { - title: 'Stats', - content: ( - - - - - - - + + + + + {user.bio && ( + <> +
+ +
+ + + )} + {(user.website || user.twitterHandle || user.discordHandle) && ( + + {user.website && ( + + + + + {user.website} + - - - ), - }, - ]} - /> + + )} + + {user.twitterHandle && ( + + + Twitter + + {user.twitterHandle} + + + + )} + + {user.discordHandle && ( + + + Discord + + {user.discordHandle} + + + + )} + + )} + , + content: ( + <> + + + + + + ), + }, + { + title: 'Markets', + tabIcon: , + content: ( + <> + + + + ), + }, + { + title: 'Comments', + tabIcon: , + content: ( + <> + + + + + + ), + }, + ]} + /> + ) @@ -314,3 +244,88 @@ export function defaultBannerUrl(userId: string) { ] return defaultBanner[genHash(userId)() % defaultBanner.length] } + +export function ProfilePrivateStats(props: { + currentUser: User | null | undefined + profit: number + user: User + router: NextRouter +}) { + const { currentUser, profit, user, router } = props + const [showBettingStreakModal, setShowBettingStreakModal] = useState(false) + const [showLoansModal, setShowLoansModal] = useState(false) + + useEffect(() => { + const showBettingStreak = router.query['show'] === 'betting-streak' + setShowBettingStreakModal(showBettingStreak) + + const showLoansModel = router.query['show'] === 'loans' + setShowLoansModal(showLoansModel) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( + <> + + + = 0 ? 'text-green-600' : 'text-red-400')} + > + {formatMoney(profit)} + + profit + + setShowBettingStreakModal(true)} + > + + 🔥 {user.currentBettingStreak ?? 0} + + + streak + + + setShowLoansModal(true)} + > + + 🏦 {formatMoney(user.nextLoanCached ?? 0)} + + next loan + + + {BettingStreakModal && ( + + )} + {showLoansModal && ( + + )} + + ) +} + +export function ProfilePublicStats(props: { user: User; className?: string }) { + const { user, className } = props + return ( + + + + {/* */} + + {/* */} + + ) +} diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index 2c095db6..caa9f47a 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -13,7 +13,6 @@ import { Page } from 'web/components/page' import { SEO } from 'web/components/SEO' import { SiteLink } from 'web/components/site-link' import { Title } from 'web/components/title' -import { defaultBannerUrl } from 'web/components/user-page' import { generateNewApiKey } from 'web/lib/api/api-key' import { changeUserInfo } from 'web/lib/firebase/api' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' @@ -176,27 +175,6 @@ export default function ProfilePage(props: { onBlur={updateUsername} /> - - {/* TODO: Allow users with M$ 2000 of assets to set custom banners */} - {/* */} - -
- {( [ ['bio', 'Bio'],