import { take, sortBy, debounce } from 'lodash' import { Group } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract } from 'web/lib/firebase/contracts' import { groupPath, getGroupBySlug, getGroupContracts, updateGroup, addContractToGroup, addUserToGroup, } from 'web/lib/firebase/groups' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' import { firebaseLogin, getUser, User, writeReferralInfo, } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' import { formatMoney } from 'common/util/format' import { EditGroupButton } from 'web/components/groups/edit-group-button' import Custom404 from '../../404' import { SEO } from 'web/components/SEO' import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' import { ContractsGrid } from 'web/components/contract/contracts-list' import { createButtonStyle, CreateQuestionButton, } from 'web/components/create-question-button' import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' import { ShareIconButton } from 'web/components/share-icon-button' import { REFERRAL_AMOUNT } from 'common/user' import { SiteLink } from 'web/components/site-link' import { ContractSearch } from 'web/components/contract-search' import clsx from 'clsx' import { FollowList } from 'web/components/follow-list' import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { const { slugs } = props.params const group = await getGroupBySlug(slugs[0]) const members = group ? await listMembers(group) : [] const creatorPromise = group ? getUser(group.creatorId) : null const contracts = group ? await getGroupContracts(group).catch((_) => []) : [] const bets = await Promise.all( contracts.map((contract: Contract) => listAllBets(contract.id)) ) const creatorScores = scoreCreators(contracts) const traderScores = scoreTraders(contracts, bets) const [topCreators, topTraders] = await Promise.all([ toTopUsers(creatorScores), toTopUsers(traderScores), ]) const creator = await creatorPromise return { props: { group, members, creator, traderScores, topTraders, creatorScores, topCreators, }, revalidate: 60, // regenerate after a minute } } async function toTopUsers(userScores: { [userId: string]: number }) { const topUserPairs = take( sortBy(Object.entries(userScores), ([_, score]) => -1 * score), 10 ).filter(([_, score]) => score >= 0.5) const topUsers = await Promise.all( topUserPairs.map(([userId]) => getUser(userId)) ) return topUsers.filter((user) => user) } export async function getStaticPaths() { return { paths: [], fallback: 'blocking' } } const groupSubpages = [ undefined, 'chat', 'questions', 'rankings', 'about', ] as const export default function GroupPage(props: { group: Group | null members: User[] creator: User traderScores: { [userId: string]: number } topTraders: User[] creatorScores: { [userId: string]: number } topCreators: User[] }) { props = usePropz(props, getStaticPropz) ?? { group: null, members: [], creator: null, traderScores: {}, topTraders: [], creatorScores: {}, topCreators: [], } const { creator, members, traderScores, topTraders, creatorScores, topCreators, } = props const router = useRouter() const { slugs } = router.query as { slugs: string[] } const page = (slugs?.[1] ?? 'chat') as typeof groupSubpages[number] const group = useGroup(props.group?.id) ?? props.group const [contracts, setContracts] = useState(undefined) const [query, setQuery] = useState('') const tips = useTipTxns({ groupId: group?.id }) const messages = useCommentsOnGroup(group?.id) const debouncedQuery = debounce(setQuery, 50) const filteredContracts = query != '' && contracts ? contracts.filter( (c) => checkAgainstQuery(query, c.question) || checkAgainstQuery(query, c.creatorName) || checkAgainstQuery(query, c.creatorUsername) ) : [] useEffect(() => { if (group) getGroupContracts(group).then((contracts) => setContracts(contracts)) }, [group]) const user = useUser() useEffect(() => { const { referrer } = router.query as { referrer?: string } if (!user && router.isReady) writeReferralInfo(creator.username, undefined, referrer, group?.slug) }, [user, creator, group, router]) if (group === null || !groupSubpages.includes(page) || slugs[2]) { return } const { memberIds } = group const isCreator = user && group && user.id === group.creatorId const isMember = user && memberIds.includes(user.id) const rightSidebar = ( ) const leaderboard = ( ) const aboutTab = ( ) return (
{group.name}
) : ( ), href: groupPath(group.slug, 'chat'), }, { title: 'Questions', content: (
{contracts ? ( contracts.length > 0 ? ( <> debouncedQuery(e.target.value)} placeholder="Search the group's questions" className="input input-bordered mb-4 w-full" /> {}} /> ) : (
No questions yet. Why not{' '} add one?
) ) : ( )}
), href: groupPath(group.slug, 'questions'), }, { title: 'Rankings', content: leaderboard, href: groupPath(group.slug, 'rankings'), }, { title: 'About', content: aboutTab, href: groupPath(group.slug, 'about'), }, ]} />
) } function JoinOrCreateButton(props: { group: Group user: User | null | undefined isMember: boolean }) { const { group, user, isMember } = props return user && isMember ? ( ) : group.anyoneCanJoin ? ( ) : null } function GroupOverview(props: { group: Group creator: User user: User | null | undefined isCreator: boolean }) { const { group, creator, isCreator, user } = props const anyoneCanJoinChoices: { [key: string]: string } = { Closed: 'false', Open: 'true', } const [anyoneCanJoin, setAnyoneCanJoin] = useState(group.anyoneCanJoin) function updateAnyoneCanJoin(newVal: boolean) { if (group.anyoneCanJoin == newVal || !isCreator) return setAnyoneCanJoin(newVal) toast.promise(updateGroup(group, { ...group, anyoneCanJoin: newVal }), { loading: 'Updating group...', success: 'Updated group!', error: "Couldn't update group", }) } return ( <>
Created by
{isCreator && }
Membership {user && user.id === creator.id ? ( updateAnyoneCanJoin(choice.toString() === 'true') } toggleClassName={'h-10'} className={'ml-2'} /> ) : ( {anyoneCanJoin ? 'Open' : 'Closed'} )} {anyoneCanJoin && user && ( Share Invite a friend and get M${REFERRAL_AMOUNT} if they sign up! )} ) } function SearchBar(props: { setQuery: (query: string) => void }) { const { setQuery } = props const debouncedQuery = debounce(setQuery, 50) return (
debouncedQuery(e.target.value)} placeholder="Find a member" className="input input-bordered mb-4 w-full pl-12" />
) } function GroupMemberSearch(props: { group: Group }) { const [query, setQuery] = useState('') const members = useMembers(props.group) // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( (m) => checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) ) return (
{matches.length > 0 && ( m.id)} /> )}
) } export function GroupMembersList(props: { group: Group }) { const { group } = props const members = useMembers(group) const maxMambersToShow = 5 if (group.memberIds.length === 1) return
return (
Other members {members.slice(0, maxMambersToShow).map((member, i) => (
{members.length > 1 && i !== members.length - 1 && ,}
))} {members.length > maxMambersToShow && ( & {members.length - maxMambersToShow} more )}
) } function SortedLeaderboard(props: { users: User[] scoreFunction: (user: User) => number title: string header: string }) { const { users, scoreFunction, title, header } = props const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a)) return ( formatMoney(scoreFunction(user)) }, ]} /> ) } function GroupLeaderboards(props: { traderScores: { [userId: string]: number } creatorScores: { [userId: string]: number } topTraders: User[] topCreators: User[] members: User[] user: User | null | undefined }) { const { traderScores, creatorScores, members, topTraders, topCreators } = props // Consider hiding M$0 // If it's just one member (curator), show all bettors, otherwise just show members return (
{members.length > 1 ? ( <> traderScores[user.id] ?? 0} title="🏅 Bettor rankings" header="Profit" /> creatorScores[user.id] ?? 0} title="🏅 Creator rankings" header="Market volume" /> ) : ( <> formatMoney(traderScores[user.id] ?? 0), }, ]} /> formatMoney(creatorScores[user.id] ?? 0), }, ]} /> )}
) } function AddContractButton(props: { group: Group; user: User }) { const { group } = props const [open, setOpen] = useState(false) async function addContractToCurrentGroup(contract: Contract) { await addContractToGroup(group, contract.id) setOpen(false) } return ( <>
Add a question to your group
) } function JoinGroupButton(props: { group: Group user: User | null | undefined }) { const { group, user } = props function joinGroup() { if (user && !group.memberIds.includes(user.id)) { toast.promise(addUserToGroup(group, user.id), { loading: 'Joining group...', success: 'Joined group!', error: "Couldn't join group", }) } } return (
) }