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:
ingawei 2022-09-26 18:01:13 -05:00 committed by GitHub
parent d612192109
commit 2fe9fe593d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 529 additions and 337 deletions

View File

@ -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">
<Col>
<div className="text-sm text-gray-500">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="justify-between gap-4 sm:flex-row">
<Col>
<div className="text-greyscale-6 text-xs sm:text-sm">
Investment value
</div>
<div className="text-lg">
{formatMoney(currentNetInvestment)}{' '}
<ProfitBadge profitPercent={investedProfitPercent} />
</div>
</Col>
<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 ? (

View File

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

View File

@ -128,6 +128,7 @@ export function CreatorContractsList(props: {
creatorId: creator.id,
}}
persistPrefix={`user-${creator.id}`}
profile={true}
/>
)
}

View File

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

View File

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

View File

@ -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}
{tab.title}
<Col>
{tab.tabIcon && <div className="mx-auto">{tab.tabIcon}</div>}
{tab.title}
</Col>
</a>
))}
</nav>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 && (
<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>
</SiteLink>
<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>
<span className="break-anywhere text-2xl font-bold">
{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)}
<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-lg font-bold sm:text-2xl">
{user.name}
</span>
<span>profit</span>
</Col>
<Col
className={clsx(
'cursor-pointer items-center text-gray-500',
isCurrentUser && !hasCompletedStreakToday(user)
? 'grayscale'
: 'grayscale-0'
)}
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 className="sm:text-md text-greyscale-4 text-sm">
@{user.username}
</span>
<span>next loan</span>
</Col>
</Row>
{isCurrentUser && (
<ProfilePrivateStats
currentUser={currentUser}
profit={profit}
user={user}
router={router}
/>
)}
{!isCurrentUser && <UserFollowButton userId={user.id} />}
</div>
<ProfilePublicStats
className="sm:text-md text-greyscale-6 hidden text-sm md:inline"
user={user}
/>
</Col>
</Row>
<Spacer h={4} />
{user.bio && (
<>
<div>
<Linkify text={user.bio}></Linkify>
</div>
<Spacer h={4} />
</>
)}
{(user.website || user.twitterHandle || user.discordHandle) && (
<Row className="mb-5 flex-wrap items-center gap-2 sm:gap-4">
{user.website && (
<SiteLink
href={
'https://' +
user.website.replace('http://', '').replace('https://', '')
}
>
<Row className="items-center gap-1">
<LinkIcon className="h-4 w-4" />
<span className="text-sm text-gray-500">{user.website}</span>
</Row>
</SiteLink>
)}
{user.twitterHandle && (
<SiteLink
href={`https://twitter.com/${user.twitterHandle
.replace('https://www.twitter.com/', '')
.replace('https://twitter.com/', '')
.replace('www.twitter.com/', '')
.replace('twitter.com/', '')}`}
>
<Row className="items-center gap-1">
<img
src="/twitter-logo.svg"
className="h-4 w-4"
alt="Twitter"
/>
<span className="text-sm text-gray-500">
{user.twitterHandle}
</span>
</Row>
</SiteLink>
)}
{user.discordHandle && (
<SiteLink href="https://discord.com/invite/eHQBNBqXuh">
<Row className="items-center gap-1">
<img
src="/discord-logo.svg"
className="h-4 w-4"
alt="Discord"
/>
<span className="text-sm text-gray-500">
{user.discordHandle}
</span>
</Row>
</SiteLink>
)}
</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 '}
tabs={[
{
title: 'Markets',
content: (
<CreatorContractsList user={currentUser} creator={user} />
),
},
{
title: 'Comments',
content: (
<Col>
<UserCommentsList user={user} />
</Col>
),
},
{
title: capitalize(PAST_BETS),
content: (
<>
<BetsList user={user} />
</>
),
},
{
title: 'Stats',
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} />
<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 className="sm:text-md mt-2 text-sm sm:mt-0">
<Linkify text={user.bio}></Linkify>
</div>
<Spacer h={2} />
</>
)}
{(user.website || user.twitterHandle || user.discordHandle) && (
<Row className="mb-2 flex-wrap items-center gap-2 sm:gap-4">
{user.website && (
<SiteLink
href={
'https://' +
user.website.replace('http://', '').replace('https://', '')
}
>
<Row className="items-center gap-1">
<LinkIcon className="h-4 w-4" />
<span className="text-greyscale-4 text-sm">
{user.website}
</span>
</Row>
<PortfolioValueSection userId={user.id} />
</Col>
),
},
]}
/>
</SiteLink>
)}
{user.twitterHandle && (
<SiteLink
href={`https://twitter.com/${user.twitterHandle
.replace('https://www.twitter.com/', '')
.replace('https://twitter.com/', '')
.replace('www.twitter.com/', '')
.replace('twitter.com/', '')}`}
>
<Row className="items-center gap-1">
<img
src="/twitter-logo.svg"
className="h-4 w-4"
alt="Twitter"
/>
<span className="text-greyscale-4 text-sm">
{user.twitterHandle}
</span>
</Row>
</SiteLink>
)}
{user.discordHandle && (
<SiteLink href="https://discord.com/invite/eHQBNBqXuh">
<Row className="items-center gap-1">
<img
src="/discord-logo.svg"
className="h-4 w-4"
alt="Discord"
/>
<span className="text-greyscale-4 text-sm">
{user.discordHandle}
</span>
</Row>
</SiteLink>
)}
</Row>
)}
<QueryUncontrolledTabs
currentPageForAnalytics={'profile'}
labelClassName={'pb-2 pt-1 sm:pt-4 '}
tabs={[
{
title: 'Portfolio',
tabIcon: <FolderIcon className="h-5" />,
content: (
<>
<Spacer h={4} />
<PortfolioValueSection userId={user.id} />
<Spacer h={4} />
<BetsList user={user} />
</>
),
},
{
title: 'Markets',
tabIcon: <ScaleIcon className="h-5" />,
content: (
<>
<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>
)
}

View File

@ -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'],