Show every user's bets on their profile (#170)

* Show user bets on their profile

* Add an alert for current users

* Replace `/portfolio` with `/Austin?tab=Bets`

* Replace `/Austin?tab=Bets` with `/Austin/bets`

* Use replaceState for better browser history

* Remove two console.logs

* Note a bug

* Fix path

* Write in description of why we're doing this
This commit is contained in:
Austin Chen 2022-05-18 10:36:17 -04:00 committed by GitHub
parent 42c981a54d
commit d50cc39c27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 26 deletions

View File

@ -19,7 +19,6 @@ export function ContractsGrid(props: {
const isBottomVisible = useIsVisible(elem)
useEffect(() => {
console.log({ isBottomVisible, hasMore })
if (isBottomVisible) {
loadMore()
}

View File

@ -47,7 +47,7 @@ export function BottomNavBar() {
)}
{user !== null && (
<Link href="/portfolio">
<Link href={`${user}/bets`}>
<a className="block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700">
<PresentationChartLineIcon
className="my-1 mx-auto h-6 w-6"

View File

@ -28,12 +28,18 @@ function IconFromUrl(url: string): React.ComponentType<{ className?: string }> {
}
}
const navigation = [
{ name: 'Home', href: '/home', icon: HomeIcon },
{ name: 'Activity', href: '/activity', icon: ChatAltIcon },
{ name: 'Portfolio', href: '/portfolio', icon: PresentationChartLineIcon },
{ name: 'Charity', href: '/charity', icon: HeartIcon },
]
function getNavigation(userName: string) {
return [
{ name: 'Home', href: '/home', icon: HomeIcon },
{ name: 'Activity', href: '/activity', icon: ChatAltIcon },
{
name: 'Portfolio',
href: `/${userName}/bets`,
icon: PresentationChartLineIcon,
},
{ name: 'Charity', href: '/charity', icon: HeartIcon },
]
}
const signedOutNavigation = [
{ name: 'Home', href: '/home', icon: HomeIcon },
@ -119,7 +125,8 @@ export default function Sidebar(props: { className?: string }) {
folds = _.sortBy(folds, 'followCount').reverse()
const deservesDailyFreeMarket = !useHasCreatedContractToday(user)
const navigationOptions = user === null ? signedOutNavigation : navigation
const navigationOptions =
user === null ? signedOutNavigation : getNavigation(user?.name || 'error')
const mobileNavigationOptions =
user === null ? signedOutMobileNavigation : mobileNavigation

View File

@ -21,6 +21,9 @@ import { getContractFromId, listContracts } from 'web/lib/firebase/contracts'
import { LoadingIndicator } from './loading-indicator'
import { useRouter } from 'next/router'
import _ from 'lodash'
import { BetsList } from './bets-list'
import { Bet } from 'common/bet'
import { getUserBets } from 'web/lib/firebase/bets'
export function UserLink(props: {
name: string
@ -38,12 +41,13 @@ export function UserLink(props: {
)
}
export const TAB_IDS = ['markets', 'comments', 'bets']
export function UserPage(props: {
user: User
currentUser?: User
defaultTabTitle?: string
defaultTabTitle?: 'markets' | 'comments' | 'bets'
}) {
const router = useRouter()
const { user, currentUser, defaultTabTitle } = props
const isCurrentUser = user.id === currentUser?.id
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
@ -51,6 +55,7 @@ export function UserPage(props: {
const [usersContracts, setUsersContracts] = useState<Contract[] | 'loading'>(
'loading'
)
const [usersBets, setUsersBets] = useState<Bet[] | 'loading'>('loading')
const [commentsByContract, setCommentsByContract] = useState<
Map<Contract, Comment[]> | 'loading'
>('loading')
@ -59,6 +64,7 @@ export function UserPage(props: {
if (!user) return
getUsersComments(user.id).then(setUsersComments)
listContracts(user.id).then(setUsersContracts)
getUserBets(user.id).then(setUsersBets)
}, [user])
useEffect(() => {
@ -187,17 +193,14 @@ export function UserPage(props: {
{usersContracts !== 'loading' && commentsByContract != 'loading' ? (
<Tabs
className={'pb-2 pt-1 '}
defaultIndex={defaultTabTitle === 'Comments' ? 1 : 0}
onClick={(tabName) =>
router.push(
{
pathname: `/${user.username}`,
query: { tab: tabName },
},
undefined,
{ shallow: true }
)
}
defaultIndex={TAB_IDS.indexOf(defaultTabTitle || 'markets')}
onClick={(tabName) => {
const tabId = tabName.toLowerCase()
const subpath = tabId === 'markets' ? '' : '/' + tabId
// BUG: if you start on `/Bob/bets`, then click on Markets, use-query-and-sort-params
// rewrites the url incorrectly to `/Bob/bets` instead of `/Bob`
window.history.replaceState('', '', `/${user.username}${subpath}`)
}}
tabs={[
{
title: 'Markets',
@ -220,6 +223,24 @@ export function UserPage(props: {
<div className="px-0.5 font-bold">{usersComments.length}</div>
),
},
{
title: 'Bets',
content: (
<div>
<AlertBox
title="Bets are becoming publicly visible on 2022-06-01"
text="Bettor identities have always been traceable through the Manifold API.
However, our interface implied that they were private.
As we develop new features such as leaderboards and bet history, it won't be technically feasible to keep this info private.
For more context, or if you'd like to wipe your bet history, see: https://manifold.markets/Austin/will-all-bets-on-manifold-be-public"
/>
{isCurrentUser && <BetsList user={user} />}
</div>
),
tabIcon: (
<div className="px-0.5 font-bold">{usersBets.length}</div>
),
},
]}
/>
) : (
@ -242,3 +263,27 @@ export function defaultBannerUrl(userId: string) {
]
return defaultBanner[genHash(userId)() % defaultBanner.length]
}
import { ExclamationIcon } from '@heroicons/react/solid'
function AlertBox(props: { title: string; text: string }) {
const { title, text } = props
return (
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
<div className="mt-2 text-sm text-yellow-700">
<Linkify text={text} />
</div>
</div>
</div>
</div>
)
}

View File

@ -47,7 +47,6 @@ export function useInitialQueryAndSort(options?: {
}
setInitialSort(localSort ?? defaultSort)
} else {
console.log('ready setting to ', sort ?? defaultSort)
setInitialSort(sort ?? defaultSort)
}
}

View File

@ -60,6 +60,12 @@ export function listenForBets(
})
}
export async function getUserBets(userId: string) {
return getValues<Bet>(
query(collectionGroup(db, 'bets'), where('userId', '==', userId))
)
}
export function listenForUserBets(
userId: string,
setBets: (bets: Bet[]) => void

View File

@ -0,0 +1,5 @@
import UserProfile from '.'
export default function UserBets() {
return <UserProfile tab="bets" />
}

View File

@ -0,0 +1,5 @@
import UserProfile from '.'
export default function UserBets() {
return <UserProfile tab="comments" />
}

View File

@ -6,10 +6,12 @@ import { UserPage } from 'web/components/user-page'
import { useUser } from 'web/hooks/use-user'
import Custom404 from '../404'
export default function UserProfile() {
export default function UserProfile(props: {
tab?: 'markets' | 'comments' | 'bets'
}) {
const router = useRouter()
const [user, setUser] = useState<User | null | 'loading'>('loading')
const { username, tab } = router.query as { username: string; tab: string }
const { username } = router.query as { username: string }
useEffect(() => {
if (username) {
getUserByUsername(username).then(setUser)
@ -24,7 +26,7 @@ export default function UserProfile() {
<UserPage
user={user}
currentUser={currentUser || undefined}
defaultTabTitle={tab}
defaultTabTitle={props.tab}
/>
) : (
<Custom404 />