Inga/profile (#937)
- Changed edit profile button - got rid of banner - merged stats and trades tab on profile - made multicolored profit graph
This commit is contained in:
parent
d612192109
commit
2fe9fe593d
|
@ -77,7 +77,7 @@ export function BetsList(props: { user: User }) {
|
|||
}, [contractList])
|
||||
|
||||
const [sort, setSort] = useState<BetSort>('newest')
|
||||
const [filter, setFilter] = useState<BetFilter>('open')
|
||||
const [filter, setFilter] = useState<BetFilter>('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 (
|
||||
<Col>
|
||||
<Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
|
||||
<Row className="gap-8">
|
||||
<Row className="justify-between gap-4 sm:flex-row">
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Investment value</div>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||
Investment value
|
||||
</div>
|
||||
<div className="text-lg">
|
||||
{formatMoney(currentNetInvestment)}{' '}
|
||||
<ProfitBadge profitPercent={investedProfitPercent} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div className="text-sm text-gray-500">Total profit</div>
|
||||
<div className="text-lg">
|
||||
{formatMoney(totalPnl)}{' '}
|
||||
<ProfitBadge profitPercent={totalProfitPercent} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="gap-8">
|
||||
<Row className="gap-2">
|
||||
<select
|
||||
className="select select-bordered self-start"
|
||||
className="border-greyscale-4 self-start overflow-hidden rounded border px-2 py-2 text-sm"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as BetFilter)}
|
||||
>
|
||||
|
@ -195,7 +186,7 @@ export function BetsList(props: { user: User }) {
|
|||
</select>
|
||||
|
||||
<select
|
||||
className="select select-bordered self-start"
|
||||
className="border-greyscale-4 self-start overflow-hidden rounded px-2 py-2 text-sm"
|
||||
value={sort}
|
||||
onChange={(e) => setSort(e.target.value as BetSort)}
|
||||
>
|
||||
|
@ -205,7 +196,7 @@ export function BetsList(props: { user: User }) {
|
|||
<option value="closeTime">Close date</option>
|
||||
</select>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Col className="mt-6 divide-y">
|
||||
{displayedContracts.length === 0 ? (
|
||||
|
|
|
@ -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 ? (
|
||||
<p className="mx-2 text-gray-500">
|
||||
This creator does not yet have any markets.
|
||||
</p>
|
||||
) : (
|
||||
<ContractsGrid
|
||||
contracts={renderedContracts}
|
||||
|
|
|
@ -128,6 +128,7 @@ export function CreatorContractsList(props: {
|
|||
creatorId: creator.id,
|
||||
}}
|
||||
persistPrefix={`user-${creator.id}`}
|
||||
profile={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<TextButton onClick={() => setIsOpen(true)}>
|
||||
<span className="font-semibold">{followingIds?.length ?? ''}</span>{' '}
|
||||
<TextButton onClick={() => setIsOpen(true)} className={className}>
|
||||
<span className={clsx('font-semibold')}>
|
||||
{followingIds?.length ?? ''}
|
||||
</span>{' '}
|
||||
Following
|
||||
</TextButton>
|
||||
|
||||
|
@ -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 (
|
||||
<>
|
||||
<TextButton onClick={() => setIsOpen(true)}>
|
||||
<TextButton onClick={() => setIsOpen(true)} className={className}>
|
||||
<span className="font-semibold">{followerIds?.length ?? ''}</span>{' '}
|
||||
Followers
|
||||
</TextButton>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<TextButton onClick={() => setIsOpen(true)}>
|
||||
<TextButton onClick={() => setIsOpen(true)} className={className}>
|
||||
<span className="font-semibold">{groups?.length ?? ''}</span> Groups
|
||||
</TextButton>
|
||||
|
||||
|
|
|
@ -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 && <span>{tab.tabIcon}</span>}
|
||||
{tab.badge ? (
|
||||
<span className="px-0.5 font-bold">{tab.badge}</span>
|
||||
) : null}
|
||||
<Col>
|
||||
{tab.tabIcon && <div className="mx-auto">{tab.tabIcon}</div>}
|
||||
{tab.title}
|
||||
</Col>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
|
|
@ -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)
|
||||
|
||||
return {
|
||||
x: new Date(timestamp),
|
||||
y: mode === 'value' ? value : profit,
|
||||
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 data = [{ id: 'Value', data: points, color: '#11b981' }]
|
||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
||||
const numYTickValues = 4
|
||||
const endDate = last(points)?.x
|
||||
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 (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }}
|
||||
style={{ height: height ?? (!width || width >= 800 ? 200 : 100) }}
|
||||
onMouseLeave={() => handleGraphDisplayChange(null)}
|
||||
>
|
||||
<ResponsiveLine
|
||||
margin={{ top: 10, right: 0, left: 40, bottom: 10 }}
|
||||
data={data}
|
||||
margin={{ top: 20, right: 28, bottom: 22, left: 60 }}
|
||||
xScale={{
|
||||
type: 'time',
|
||||
min: points[0]?.x,
|
||||
min: valuePoints[0]?.x,
|
||||
max: endDate,
|
||||
}}
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
stacked: false,
|
||||
min: Math.min(...points.map((p) => 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 (
|
||||
<div className="rounded bg-white px-4 py-2 opacity-80">
|
||||
<div
|
||||
key={slice.points[0].id}
|
||||
className="text-xs font-semibold sm:text-sm"
|
||||
>
|
||||
<Col>
|
||||
<div>
|
||||
{dayjs(slice.points[0].data.xFormatted).format('MMM/D/YY')}
|
||||
</div>
|
||||
<div className="text-greyscale-6 text-2xs font-normal sm:text-xs">
|
||||
{dayjs(slice.points[0].data.xFormatted).format('h:mm A')}
|
||||
</div>
|
||||
</Col>
|
||||
</div>
|
||||
{/* ))} */}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
></ResponsiveLine>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<Period>('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 (
|
||||
<>
|
||||
<Row className="gap-8">
|
||||
<Col className="flex-1 justify-center">
|
||||
<div className="text-sm text-gray-500">Profit</div>
|
||||
<div className="text-lg">{formatMoney(totalProfit)}</div>
|
||||
</Col>
|
||||
<select
|
||||
className="select select-bordered self-start"
|
||||
value={portfolioPeriod}
|
||||
onChange={(e) => {
|
||||
setPortfolioPeriod(e.target.value as Period)
|
||||
}}
|
||||
<Row className="mb-2 justify-between">
|
||||
<Row className="gap-4 sm:gap-8">
|
||||
<Col
|
||||
className={clsx(
|
||||
'cursor-pointer',
|
||||
graphMode != 'value' ? 'opacity-40 hover:opacity-80' : ''
|
||||
)}
|
||||
onClick={() => setGraphMode('value')}
|
||||
>
|
||||
<option value="allTime">All time</option>
|
||||
<option value="monthly">Last Month</option>
|
||||
<option value="weekly">Last 7d</option>
|
||||
<option value="daily">Last 24h</option>
|
||||
</select>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">
|
||||
Portfolio value
|
||||
</div>
|
||||
<div className={clsx('text-lg text-indigo-600 sm:text-xl')}>
|
||||
{graphMode === 'value'
|
||||
? graphDisplayNumber
|
||||
? graphDisplayNumber
|
||||
: formatMoney(totalValue)
|
||||
: formatMoney(totalValue)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col
|
||||
className={clsx(
|
||||
'cursor-pointer',
|
||||
graphMode != 'profit'
|
||||
? 'cursor-pointer opacity-40 hover:opacity-80'
|
||||
: ''
|
||||
)}
|
||||
onClick={() => setGraphMode('profit')}
|
||||
>
|
||||
<div className="text-greyscale-6 text-xs sm:text-sm">Profit</div>
|
||||
<div
|
||||
className={clsx(
|
||||
graphMode === 'profit'
|
||||
? graphDisplayNumber
|
||||
? graphDisplayNumber.toString().includes('-')
|
||||
? 'text-red-600'
|
||||
: 'text-teal-500'
|
||||
: totalProfit > 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)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
<PortfolioValueGraph
|
||||
portfolioHistory={currPortfolioHistory}
|
||||
includeTime={portfolioPeriod == 'daily'}
|
||||
mode="profit"
|
||||
mode={graphMode}
|
||||
handleGraphDisplayChange={handleGraphDisplayChange}
|
||||
/>
|
||||
<Spacer h={8} />
|
||||
<Col className="flex-1 justify-center">
|
||||
<div className="text-sm text-gray-500">Portfolio value</div>
|
||||
<div className="text-lg">{formatMoney(totalValue)}</div>
|
||||
</Col>
|
||||
<PortfolioValueGraph
|
||||
portfolioHistory={currPortfolioHistory}
|
||||
includeTime={portfolioPeriod == 'daily'}
|
||||
mode="value"
|
||||
<PortfolioPeriodSelection
|
||||
portfolioPeriod={portfolioPeriod}
|
||||
setPortfolioPeriod={setPortfolioPeriod}
|
||||
className="border-greyscale-2 mt-2 gap-4 border-b"
|
||||
selectClassName="text-indigo-600 text-bold border-b border-indigo-600"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export function PortfolioPeriodSelection(props: {
|
||||
setPortfolioPeriod: (string: any) => void
|
||||
portfolioPeriod: string
|
||||
className?: string
|
||||
selectClassName?: string
|
||||
}) {
|
||||
const { setPortfolioPeriod, portfolioPeriod, className, selectClassName } =
|
||||
props
|
||||
return (
|
||||
<Row className={clsx(className, 'text-greyscale-4')}>
|
||||
<button
|
||||
className={clsx(portfolioPeriod === 'daily' ? selectClassName : '')}
|
||||
onClick={() => setPortfolioPeriod('daily' as Period)}
|
||||
>
|
||||
1D
|
||||
</button>
|
||||
<button
|
||||
className={clsx(portfolioPeriod === 'weekly' ? selectClassName : '')}
|
||||
onClick={() => setPortfolioPeriod('weekly' as Period)}
|
||||
>
|
||||
1W
|
||||
</button>
|
||||
<button
|
||||
className={clsx(portfolioPeriod === 'monthly' ? selectClassName : '')}
|
||||
onClick={() => setPortfolioPeriod('monthly' as Period)}
|
||||
>
|
||||
1M
|
||||
</button>
|
||||
<button
|
||||
className={clsx(portfolioPeriod === 'allTime' ? selectClassName : '')}
|
||||
onClick={() => setPortfolioPeriod('allTime' as Period)}
|
||||
>
|
||||
ALL
|
||||
</button>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export function GraphToggle(props: {
|
||||
setGraphMode: (mode: 'profit' | 'value') => void
|
||||
graphMode: string
|
||||
}) {
|
||||
const { setGraphMode, graphMode } = props
|
||||
return (
|
||||
<Row className="relative mt-1 ml-1 items-center gap-1.5 sm:ml-0 sm:gap-2">
|
||||
<PillButton
|
||||
selected={graphMode === 'value'}
|
||||
onSelect={() => {
|
||||
setGraphMode('value')
|
||||
}}
|
||||
xs={true}
|
||||
className="z-50"
|
||||
>
|
||||
Value
|
||||
</PillButton>
|
||||
<PillButton
|
||||
selected={graphMode === 'profit'}
|
||||
onSelect={() => {
|
||||
setGraphMode('profit')
|
||||
}}
|
||||
xs={true}
|
||||
className="z-50"
|
||||
>
|
||||
Profit
|
||||
</PillButton>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<TextButton onClick={() => setIsOpen(true)}>
|
||||
<TextButton onClick={() => setIsOpen(true)} className={className}>
|
||||
<span className="font-semibold">{likedContracts?.length ?? ''}</span>{' '}
|
||||
Likes
|
||||
</TextButton>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<TextButton onClick={() => setIsOpen(true)}>
|
||||
<TextButton onClick={() => setIsOpen(true)} className={className}>
|
||||
<span className="font-semibold">{referralIds?.length ?? ''}</span>{' '}
|
||||
Referrals
|
||||
</TextButton>
|
||||
|
|
|
@ -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,102 +74,65 @@ export function UserPage(props: { user: User }) {
|
|||
{showConfetti && (
|
||||
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||
)}
|
||||
<BettingStreakModal
|
||||
isOpen={showBettingStreakModal}
|
||||
setOpen={setShowBettingStreakModal}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
{showLoansModal && (
|
||||
<LoansModal isOpen={showLoansModal} setOpen={setShowLoansModal} />
|
||||
)}
|
||||
{/* Banner image up top, with an circle avatar overlaid */}
|
||||
<div
|
||||
className="h-32 w-full bg-cover bg-center sm:h-40"
|
||||
style={{
|
||||
backgroundImage: `url(${bannerUrl})`,
|
||||
}}
|
||||
></div>
|
||||
<div className="relative mb-20">
|
||||
<div className="absolute -top-10 left-4">
|
||||
<Col className="relative">
|
||||
<Row className="relative px-4 pt-4">
|
||||
<Avatar
|
||||
username={user.username}
|
||||
avatarUrl={user.avatarUrl}
|
||||
size={24}
|
||||
className="bg-white ring-4 ring-white"
|
||||
className="bg-white shadow-sm shadow-indigo-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top right buttons (e.g. edit, follow) */}
|
||||
<div className="absolute right-0 top-0 mt-2 mr-4">
|
||||
{!isCurrentUser && <UserFollowButton userId={user.id} />}
|
||||
{isCurrentUser && (
|
||||
<SiteLink className="btn-sm btn" href="/profile">
|
||||
<PencilIcon className="h-5 w-5" />{' '}
|
||||
<div className="ml-2">Edit</div>
|
||||
<div className="absolute z-50 ml-16 mt-16 rounded-full bg-indigo-600 p-2 text-white shadow-sm shadow-indigo-300">
|
||||
<SiteLink href="/profile">
|
||||
<PencilIcon className="h-5" />{' '}
|
||||
</SiteLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
||||
<Col className="mx-4 -mt-6">
|
||||
<Row className={'flex-wrap justify-between gap-y-2'}>
|
||||
<Col className="w-full gap-4 pl-5">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:justify-between">
|
||||
<Col>
|
||||
<span className="break-anywhere text-2xl font-bold">
|
||||
<span className="break-anywhere text-lg font-bold sm:text-2xl">
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="text-gray-500">@{user.username}</span>
|
||||
</Col>
|
||||
<Col className={'justify-center'}>
|
||||
<Row className={'gap-3'}>
|
||||
<Col className={'items-center text-gray-500'}>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-md',
|
||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{formatMoney(profit)}
|
||||
<span className="sm:text-md text-greyscale-4 text-sm">
|
||||
@{user.username}
|
||||
</span>
|
||||
<span>profit</span>
|
||||
</Col>
|
||||
<Col
|
||||
className={clsx(
|
||||
'cursor-pointer items-center text-gray-500',
|
||||
isCurrentUser && !hasCompletedStreakToday(user)
|
||||
? 'grayscale'
|
||||
: 'grayscale-0'
|
||||
{isCurrentUser && (
|
||||
<ProfilePrivateStats
|
||||
currentUser={currentUser}
|
||||
profit={profit}
|
||||
user={user}
|
||||
router={router}
|
||||
/>
|
||||
)}
|
||||
onClick={() => setShowBettingStreakModal(true)}
|
||||
>
|
||||
<span>🔥 {user.currentBettingStreak ?? 0}</span>
|
||||
<span>streak</span>
|
||||
</Col>
|
||||
<Col
|
||||
className={
|
||||
'flex-shrink-0 cursor-pointer items-center text-gray-500'
|
||||
}
|
||||
onClick={() => setShowLoansModal(true)}
|
||||
>
|
||||
<span className="text-green-600">
|
||||
🏦 {formatMoney(user.nextLoanCached ?? 0)}
|
||||
</span>
|
||||
<span>next loan</span>
|
||||
{!isCurrentUser && <UserFollowButton userId={user.id} />}
|
||||
</div>
|
||||
<ProfilePublicStats
|
||||
className="sm:text-md text-greyscale-6 hidden text-sm md:inline"
|
||||
user={user}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Spacer h={4} />
|
||||
<Col className="mx-4 mt-2">
|
||||
<Spacer h={1} />
|
||||
<ProfilePublicStats
|
||||
className="text-greyscale-6 text-sm md:hidden"
|
||||
user={user}
|
||||
/>
|
||||
<Spacer h={1} />
|
||||
{user.bio && (
|
||||
<>
|
||||
<div>
|
||||
<div className="sm:text-md mt-2 text-sm sm:mt-0">
|
||||
<Linkify text={user.bio}></Linkify>
|
||||
</div>
|
||||
<Spacer h={4} />
|
||||
<Spacer h={2} />
|
||||
</>
|
||||
)}
|
||||
{(user.website || user.twitterHandle || user.discordHandle) && (
|
||||
<Row className="mb-5 flex-wrap items-center gap-2 sm:gap-4">
|
||||
<Row className="mb-2 flex-wrap items-center gap-2 sm:gap-4">
|
||||
{user.website && (
|
||||
<SiteLink
|
||||
href={
|
||||
|
@ -190,7 +142,9 @@ export function UserPage(props: { user: User }) {
|
|||
>
|
||||
<Row className="items-center gap-1">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span className="text-sm text-gray-500">{user.website}</span>
|
||||
<span className="text-greyscale-4 text-sm">
|
||||
{user.website}
|
||||
</span>
|
||||
</Row>
|
||||
</SiteLink>
|
||||
)}
|
||||
|
@ -209,7 +163,7 @@ export function UserPage(props: { user: User }) {
|
|||
className="h-4 w-4"
|
||||
alt="Twitter"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
<span className="text-greyscale-4 text-sm">
|
||||
{user.twitterHandle}
|
||||
</span>
|
||||
</Row>
|
||||
|
@ -224,7 +178,7 @@ export function UserPage(props: { user: User }) {
|
|||
className="h-4 w-4"
|
||||
alt="Discord"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
<span className="text-greyscale-4 text-sm">
|
||||
{user.discordHandle}
|
||||
</span>
|
||||
</Row>
|
||||
|
@ -232,72 +186,48 @@ export function UserPage(props: { user: User }) {
|
|||
)}
|
||||
</Row>
|
||||
)}
|
||||
{currentUser?.id === user.id && REFERRAL_AMOUNT > 0 && (
|
||||
<Row
|
||||
className={
|
||||
'mb-5 w-full items-center justify-center gap-2 rounded-md border-2 border-indigo-100 bg-indigo-50 p-2 text-indigo-600'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<SiteLink href="/referrals">
|
||||
Earn {formatMoney(REFERRAL_AMOUNT)} when you refer a friend!
|
||||
</SiteLink>{' '}
|
||||
You've gotten{' '}
|
||||
<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>
|
||||
)}
|
||||
<QueryUncontrolledTabs
|
||||
className="mb-4"
|
||||
currentPageForAnalytics={'profile'}
|
||||
labelClassName={'pb-2 pt-1 '}
|
||||
labelClassName={'pb-2 pt-1 sm:pt-4 '}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Markets',
|
||||
content: (
|
||||
<CreatorContractsList user={currentUser} creator={user} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Comments',
|
||||
content: (
|
||||
<Col>
|
||||
<UserCommentsList user={user} />
|
||||
</Col>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: capitalize(PAST_BETS),
|
||||
title: 'Portfolio',
|
||||
tabIcon: <FolderIcon className="h-5" />,
|
||||
content: (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<PortfolioValueSection userId={user.id} />
|
||||
<Spacer h={4} />
|
||||
<BetsList user={user} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Stats',
|
||||
title: 'Markets',
|
||||
tabIcon: <ScaleIcon className="h-5" />,
|
||||
content: (
|
||||
<Col className="mb-8">
|
||||
<Row className="mb-8 flex-wrap items-center gap-x-6 gap-y-2">
|
||||
<FollowingButton user={user} />
|
||||
<FollowersButton user={user} />
|
||||
<ReferralsButton user={user} />
|
||||
<GroupsButton user={user} />
|
||||
<UserLikesButton user={user} />
|
||||
</Row>
|
||||
<PortfolioValueSection userId={user.id} />
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<CreatorContractsList user={currentUser} creator={user} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Comments',
|
||||
tabIcon: <ChatIcon className="h-5" />,
|
||||
content: (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<Col>
|
||||
<UserCommentsList user={user} />
|
||||
</Col>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Row className={'justify-between gap-4 sm:justify-end'}>
|
||||
<Col className={'text-greyscale-4 text-md sm:text-lg'}>
|
||||
<span
|
||||
className={clsx(profit >= 0 ? 'text-green-600' : 'text-red-400')}
|
||||
>
|
||||
{formatMoney(profit)}
|
||||
</span>
|
||||
<span className="mx-auto text-xs sm:text-sm">profit</span>
|
||||
</Col>
|
||||
<Col
|
||||
className={clsx('text-,d cursor-pointer sm:text-lg ')}
|
||||
onClick={() => setShowBettingStreakModal(true)}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
!hasCompletedStreakToday(user)
|
||||
? 'opacity-50 grayscale'
|
||||
: 'grayscale-0'
|
||||
)}
|
||||
>
|
||||
🔥 {user.currentBettingStreak ?? 0}
|
||||
</span>
|
||||
<span className="text-greyscale-4 mx-auto text-xs sm:text-sm">
|
||||
streak
|
||||
</span>
|
||||
</Col>
|
||||
<Col
|
||||
className={
|
||||
'text-greyscale-4 text-md flex-shrink-0 cursor-pointer sm:text-lg'
|
||||
}
|
||||
onClick={() => setShowLoansModal(true)}
|
||||
>
|
||||
<span className="text-green-600">
|
||||
🏦 {formatMoney(user.nextLoanCached ?? 0)}
|
||||
</span>
|
||||
<span className="mx-auto text-xs sm:text-sm">next loan</span>
|
||||
</Col>
|
||||
</Row>
|
||||
{BettingStreakModal && (
|
||||
<BettingStreakModal
|
||||
isOpen={showBettingStreakModal}
|
||||
setOpen={setShowBettingStreakModal}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
{showLoansModal && (
|
||||
<LoansModal isOpen={showLoansModal} setOpen={setShowLoansModal} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfilePublicStats(props: { user: User; className?: string }) {
|
||||
const { user, className } = props
|
||||
return (
|
||||
<Row className={'flex-wrap items-center gap-3'}>
|
||||
<FollowingButton user={user} className={className} />
|
||||
<FollowersButton user={user} className={className} />
|
||||
{/* <ReferralsButton user={user} className={className} /> */}
|
||||
<GroupsButton user={user} className={className} />
|
||||
{/* <UserLikesButton user={user} className={className} /> */}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* TODO: Allow users with M$ 2000 of assets to set custom banners */}
|
||||
{/* <EditUserField
|
||||
user={user}
|
||||
field="bannerUrl"
|
||||
label="Banner Url"
|
||||
isEditing={isEditing}
|
||||
/> */}
|
||||
<label className="label">
|
||||
Banner image{' '}
|
||||
<span className="text-sm text-gray-400">Not editable for now</span>
|
||||
</label>
|
||||
<div
|
||||
className="h-32 w-full bg-cover bg-center sm:h-40"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
user.bannerUrl || defaultBannerUrl(user.id)
|
||||
})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{(
|
||||
[
|
||||
['bio', 'Bio'],
|
||||
|
|
Loading…
Reference in New Issue
Block a user