diff --git a/web/components/charity/feed-items.tsx b/web/components/charity/feed-items.tsx index 9216e776..43d03c7b 100644 --- a/web/components/charity/feed-items.tsx +++ b/web/components/charity/feed-items.tsx @@ -3,7 +3,7 @@ import { Avatar } from '../avatar' import { useUserById } from '../../hooks/use-users' import { UserLink } from '../user-page' import { manaToUSD } from '../../pages/charity/[charitySlug]' -import { RelativeTimestamp } from '../feed/feed-items' +import { RelativeTimestamp } from '../relative-timestamp' export function Donation(props: { txn: Txn }) { const { txn } = props diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx new file mode 100644 index 00000000..7461c6f1 --- /dev/null +++ b/web/components/comments-list.tsx @@ -0,0 +1,65 @@ +import { Comment } from '../../common/comment' +import { Contract } from '../../common/contract' +import { contractPath } from '../lib/firebase/contracts' +import { SiteLink } from './site-link' +import { Row } from './layout/row' +import { Avatar } from './avatar' +import { RelativeTimestamp } from './relative-timestamp' +import { UserLink } from './user-page' +import { User } from '../../common/user' +import { Col } from './layout/col' +import { Linkify } from './linkify' + +export function UserCommentsList(props: { + user: User + commentsByUniqueContracts: Map +}) { + const { commentsByUniqueContracts } = props + + return ( + + {Array.from(commentsByUniqueContracts).map(([contract, comments]) => ( +
+
+ + {contract.question} + +
+ {comments.map((comment) => ( +
+
+ +
+
+ ))} +
+ ))} + + ) +} + +function ProfileComment(props: { comment: Comment }) { + const { comment } = props + const { text, userUsername, userName, userAvatarUrl, createdTime } = comment + // TODO: find and attach relevant bets by comment betId at some point + return ( +
+ + +
+
+

+ {' '} + +

+
+ +
+
+
+ ) +} diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-list.tsx index dab8613d..e84af1ea 100644 --- a/web/components/contract/contracts-list.tsx +++ b/web/components/contract/contracts-list.tsx @@ -341,19 +341,8 @@ export function SearchableGrid(props: { ) } -export function CreatorContractsList(props: { creator: User }) { - const { creator } = props - const [contracts, setContracts] = useState('loading') - - useEffect(() => { - if (creator?.id) { - // TODO: stream changes from firestore - listContracts(creator.id).then(setContracts) - } - }, [creator]) - - if (contracts === 'loading') return <> - +export function CreatorContractsList(props: { contracts: Contract[] }) { + const { contracts } = props return ( - - {fromNow(time)} - - - ) -} - function getBettorsPosition( contract: Contract, createdTime: number, diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 45b6a7d7..fc9dd775 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx' import Link from 'next/link' import { useState } from 'react' +import { Row } from './row' type Tab = { title: string @@ -10,8 +11,13 @@ type Tab = { href?: string } -export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) { - const { tabs, defaultIndex } = props +export function Tabs(props: { + tabs: Tab[] + defaultIndex?: number + className?: string + onClick?: (tabName: string) => void +}) { + const { tabs, defaultIndex, className, onClick } = props const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const activeTab = tabs[activeIndex] @@ -28,19 +34,21 @@ export function Tabs(props: { tabs: Tab[]; defaultIndex?: number }) { e.preventDefault() } setActiveIndex(i) + onClick?.(tab.title) }} className={clsx( activeIndex === i ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', - 'cursor-pointer whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium' + 'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium', + className )} aria-current={activeIndex === i ? 'page' : undefined} > - {tab.tabIcon ? ( - {tab.tabIcon} - ) : null} - {tab.title} + + {tab.tabIcon && {tab.tabIcon}} + {tab.title} + ))} diff --git a/web/components/relative-timestamp.tsx b/web/components/relative-timestamp.tsx new file mode 100644 index 00000000..cd5ca713 --- /dev/null +++ b/web/components/relative-timestamp.tsx @@ -0,0 +1,14 @@ +import { DateTimeTooltip } from './datetime-tooltip' +import { fromNow } from '../lib/util/time' +import React from 'react' + +export function RelativeTimestamp(props: { time: number }) { + const { time } = props + return ( + + + {fromNow(time)} + + + ) +} diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index b73cffc4..0f033ce3 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -12,6 +12,15 @@ import { Row } from './layout/row' import { LinkIcon } from '@heroicons/react/solid' import { genHash } from '../../common/util/random' import { PencilIcon } from '@heroicons/react/outline' +import { Tabs } from './layout/tabs' +import { UserCommentsList } from './comments-list' +import { useEffect, useState } from 'react' +import { Comment, getUsersComments } from '../lib/firebase/comments' +import { Contract } from '../../common/contract' +import { getContractFromId, listContracts } from '../lib/firebase/contracts' +import { LoadingIndicator } from './loading-indicator' +import { useRouter } from 'next/router' +import _ from 'lodash' export function UserLink(props: { name: string @@ -29,10 +38,47 @@ export function UserLink(props: { ) } -export function UserPage(props: { user: User; currentUser?: User }) { - const { user, currentUser } = props +export function UserPage(props: { + user: User + currentUser?: User + defaultTabTitle?: string +}) { + const router = useRouter() + const { user, currentUser, defaultTabTitle } = props const isCurrentUser = user.id === currentUser?.id const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) + const [usersComments, setUsersComments] = useState([] as Comment[]) + const [usersContracts, setUsersContracts] = useState( + 'loading' + ) + const [commentsByContract, setCommentsByContract] = useState< + Map | 'loading' + >('loading') + + useEffect(() => { + if (!user) return + getUsersComments(user.id).then(setUsersComments) + listContracts(user.id).then(setUsersContracts) + }, [user]) + + useEffect(() => { + const uniqueContractIds = _.uniq( + usersComments.map((comment) => comment.contractId) + ) + Promise.all( + uniqueContractIds.map((contractId) => getContractFromId(contractId)) + ).then((contracts) => { + const commentsByContract = new Map() + contracts.forEach((contract) => { + if (!contract) return + commentsByContract.set( + contract, + usersComments.filter((comment) => comment.contractId === contract.id) + ) + }) + setCommentsByContract(commentsByContract) + }) + }, [usersComments]) return ( @@ -138,8 +184,59 @@ export function UserPage(props: { user: User; currentUser?: User }) { - - + {usersContracts !== 'loading' && commentsByContract != 'loading' ? ( + + router.push( + { + pathname: `/${user.username}`, + query: { tab: tabName }, + }, + undefined, + { shallow: true } + ) + } + tabs={[ + { + title: 'Markets', + content: , + tabIcon: ( +
9 ? 'px-1' : 'px-1.5', + 'items-center rounded-full border-2 border-current py-0.5 text-xs' + )} + > + {usersContracts.length} +
+ ), + }, + { + title: 'Comments', + content: ( + + ), + tabIcon: ( +
9 ? 'px-1' : 'px-1.5', + 'items-center rounded-full border-2 border-current py-0.5 text-xs' + )} + > + {usersComments.length} +
+ ), + }, + ]} + /> + ) : ( + + )}
) diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index 381269d2..e97e6f99 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -1,11 +1,11 @@ import { - doc, collection, - setDoc, - query, collectionGroup, - where, + doc, orderBy, + query, + setDoc, + where, } from 'firebase/firestore' import _ from 'lodash' @@ -13,6 +13,7 @@ import { getValues, listenForValues } from './utils' import { db } from './init' import { User } from '../../../common/user' import { Comment } from '../../../common/comment' + export type { Comment } export const MAX_COMMENT_LENGTH = 10000 @@ -125,3 +126,13 @@ export async function getDailyComments( return commentsByDay } + +const getUsersCommentsQuery = (userId: string) => + query( + collectionGroup(db, 'comments'), + where('userId', '==', userId), + orderBy('createdTime', 'desc') + ) +export async function getUsersComments(userId: string) { + return await getValues(getUsersCommentsQuery(userId)) +} diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index de88d52a..e1e63260 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -9,7 +9,7 @@ import Custom404 from '../404' export default function UserProfile() { const router = useRouter() const [user, setUser] = useState('loading') - const { username } = router.query as { username: string } + const { username, tab } = router.query as { username: string; tab: string } useEffect(() => { if (username) { getUserByUsername(username).then(setUser) @@ -21,7 +21,11 @@ export default function UserProfile() { if (user === 'loading') return <> return user ? ( - + ) : ( )