Small groups UX changes

This commit is contained in:
Ian Philips 2022-07-06 17:24:53 -06:00
parent 2591655269
commit a23c744c3e
5 changed files with 113 additions and 64 deletions

View File

@ -3,6 +3,8 @@ import clsx from 'clsx'
import { firebaseLogin, User } from 'web/lib/firebase/users' import { firebaseLogin, User } from 'web/lib/firebase/users'
import React from 'react' import React from 'react'
export const createButtonStyle =
'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0 h-11'
export const CreateQuestionButton = (props: { export const CreateQuestionButton = (props: {
user: User | null | undefined user: User | null | undefined
overrideText?: string overrideText?: string
@ -12,20 +14,20 @@ export const CreateQuestionButton = (props: {
const gradient = const gradient =
'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700' 'from-indigo-500 to-blue-500 hover:from-indigo-700 hover:to-blue-700'
const buttonStyle =
'border-w-0 mx-auto mt-4 -ml-1 w-full rounded-md bg-gradient-to-r py-2.5 text-base font-semibold text-white shadow-sm lg:-ml-0'
const { user, overrideText, className, query } = props const { user, overrideText, className, query } = props
return ( return (
<div className={clsx('aligncenter flex justify-center', className)}> <div className={clsx('flex justify-center', className)}>
{user ? ( {user ? (
<Link href={`/create${query ? query : ''}`} passHref> <Link href={`/create${query ? query : ''}`} passHref>
<button className={clsx(gradient, buttonStyle)}> <button className={clsx(gradient, createButtonStyle)}>
{overrideText ? overrideText : 'Create a question'} {overrideText ? overrideText : 'Create a question'}
</button> </button>
</Link> </Link>
) : ( ) : (
<button onClick={firebaseLogin} className={clsx(gradient, buttonStyle)}> <button
onClick={firebaseLogin}
className={clsx(gradient, createButtonStyle)}
>
Sign in Sign in
</button> </button>
)} )}

View File

@ -36,6 +36,7 @@ export function GroupChat(props: {
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null) const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
const [groupedMessages, setGroupedMessages] = useState<Comment[]>([]) const [groupedMessages, setGroupedMessages] = useState<Comment[]>([])
const router = useRouter() const router = useRouter()
const isMember = user && group.memberIds.includes(user?.id)
useMemo(() => { useMemo(() => {
// Group messages with createdTime within 2 minutes of each other. // Group messages with createdTime within 2 minutes of each other.
@ -120,7 +121,7 @@ export function GroupChat(props: {
))} ))}
{messages.length === 0 && ( {messages.length === 0 && (
<div className="p-2 text-gray-500"> <div className="p-2 text-gray-500">
No messages yet. Why not{' '} No messages yet. Why not{isMember ? ` ` : ' join and '}
<button <button
className={'cursor-pointer font-bold text-gray-700'} className={'cursor-pointer font-bold text-gray-700'}
onClick={() => focusInput()} onClick={() => focusInput()}

View File

@ -22,7 +22,7 @@ export const groups = coll<Group>('groups')
export function groupPath( export function groupPath(
groupSlug: string, groupSlug: string,
subpath?: 'edit' | 'questions' | 'about' | 'chat' subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings'
) { ) {
return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}`
} }

View File

@ -21,7 +21,6 @@ import {
User, User,
writeReferralInfo, writeReferralInfo,
} from 'web/lib/firebase/users' } from 'web/lib/firebase/users'
import { Spacer } from 'web/components/layout/spacer'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { listMembers, useGroup, useMembers } from 'web/hooks/use-group'
@ -36,7 +35,10 @@ import { Linkify } from 'web/components/linkify'
import { fromPropz, usePropz } from 'web/hooks/use-propz' import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { Tabs } from 'web/components/layout/tabs' import { Tabs } from 'web/components/layout/tabs'
import { ContractsGrid } from 'web/components/contract/contracts-list' import { ContractsGrid } from 'web/components/contract/contracts-list'
import { CreateQuestionButton } from 'web/components/create-question-button' import {
createButtonStyle,
CreateQuestionButton,
} from 'web/components/create-question-button'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { GroupChat } from 'web/components/groups/group-chat' import { GroupChat } from 'web/components/groups/group-chat'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
@ -45,11 +47,13 @@ import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useCommentsOnGroup } from 'web/hooks/use-comments' import { useCommentsOnGroup } from 'web/hooks/use-comments'
import ShortToggle from 'web/components/widgets/short-toggle'
import { ShareIconButton } from 'web/components/share-icon-button' import { ShareIconButton } from 'web/components/share-icon-button'
import { REFERRAL_AMOUNT } from 'common/user' import { REFERRAL_AMOUNT } from 'common/user'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { ContractSearch } from 'web/components/contract-search' import { ContractSearch } from 'web/components/contract-search'
import clsx from 'clsx'
import { FollowList } from 'web/components/follow-list'
import { SearchIcon } from '@heroicons/react/outline'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) { export async function getStaticPropz(props: { params: { slugs: string[] } }) {
@ -104,7 +108,13 @@ async function toTopUsers(userScores: { [userId: string]: number }) {
export async function getStaticPaths() { export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' } return { paths: [], fallback: 'blocking' }
} }
const groupSubpages = [undefined, 'chat', 'questions', 'about'] as const const groupSubpages = [
undefined,
'chat',
'questions',
'rankings',
'about',
] as const
export default function GroupPage(props: { export default function GroupPage(props: {
group: Group | null group: Group | null
@ -178,20 +188,18 @@ export default function GroupPage(props: {
const rightSidebar = ( const rightSidebar = (
<Col className="mt-6 hidden xl:block"> <Col className="mt-6 hidden xl:block">
<JoinOrCreateButton group={group} user={user} isMember={!!isMember} /> <JoinOrCreateButton group={group} user={user} isMember={!!isMember} />
<Spacer h={6} /> </Col>
{contracts && ( )
<div className={'mt-2'}> const leaderboard = (
<div className={'my-2 text-gray-500'}>Recent Questions</div> <Col>
<ContractsGrid <GroupLeaderboards
contracts={contracts traderScores={traderScores}
.sort((a, b) => b.createdTime - a.createdTime) creatorScores={creatorScores}
.slice(0, 3)} topTraders={topTraders}
hasMore={false} topCreators={topCreators}
loadMore={() => {}} members={members}
overrideGridClassName={'grid w-full grid-cols-1 gap-4'} user={user}
/> />
</div>
)}
</Col> </Col>
) )
@ -203,16 +211,6 @@ export default function GroupPage(props: {
isCreator={!!isCreator} isCreator={!!isCreator}
user={user} user={user}
/> />
<Spacer h={8} />
<GroupLeaderboards
traderScores={traderScores}
creatorScores={creatorScores}
topTraders={topTraders}
topCreators={topCreators}
members={members}
user={user}
/>
</Col> </Col>
) )
return ( return (
@ -243,7 +241,15 @@ export default function GroupPage(props: {
</Col> </Col>
<Tabs <Tabs
defaultIndex={page === 'about' ? 2 : page === 'questions' ? 1 : 0} defaultIndex={
page === 'rankings'
? 2
: page === 'about'
? 3
: page === 'questions'
? 1
: 0
}
tabs={[ tabs={[
{ {
title: 'Chat', title: 'Chat',
@ -287,11 +293,15 @@ export default function GroupPage(props: {
) : ( ) : (
<LoadingIndicator /> <LoadingIndicator />
)} )}
{isMember && <AddContractButton group={group} user={user} />}
</div> </div>
), ),
href: groupPath(group.slug, 'questions'), href: groupPath(group.slug, 'questions'),
}, },
{
title: 'Leaderboards',
content: leaderboard,
href: groupPath(group.slug, 'rankings'),
},
{ {
title: 'About', title: 'About',
content: aboutTab, content: aboutTab,
@ -309,13 +319,16 @@ function JoinOrCreateButton(props: {
isMember: boolean isMember: boolean
}) { }) {
const { group, user, isMember } = props const { group, user, isMember } = props
return isMember ? ( return user && isMember ? (
<Row className={'justify-between sm:flex-col sm:justify-center'}>
<CreateQuestionButton <CreateQuestionButton
user={user} user={user}
overrideText={'Add a new question'} overrideText={'Add a new question'}
className={'w-48 flex-shrink-0'} className={'w-48 flex-shrink-0'}
query={`?groupId=${group.id}`} query={`?groupId=${group.id}`}
/> />
<AddContractButton group={group} user={user} />
</Row>
) : group.anyoneCanJoin ? ( ) : group.anyoneCanJoin ? (
<JoinGroupButton group={group} user={user} /> <JoinGroupButton group={group} user={user} />
) : null ) : null
@ -389,8 +402,48 @@ function GroupOverview(props: {
</ShareIconButton> </ShareIconButton>
</Row> </Row>
)} )}
<Col className={'mt-2'}>
<GroupMemberSearch group={group} />
</Col> </Col>
</Col> </Col>
</Col>
)
}
function SearchBar(props: { setQuery: (query: string) => void }) {
const { setQuery } = props
const debouncedQuery = debounce(setQuery, 50)
return (
<div className={'relative'}>
<SearchIcon className={'absolute left-5 top-3.5 h-5 w-5 text-gray-500'} />
<input
type="text"
onChange={(e) => debouncedQuery(e.target.value)}
placeholder="Find a member"
className="input input-bordered mb-4 w-full pl-12"
/>
</div>
)
}
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 (
<div>
<SearchBar setQuery={setQuery} />
<Col className={'gap-2'}>
{matches.length > 0 && (
<FollowList userIds={matches.map((m) => m.id)} />
)}
</Col>
</div>
) )
} }
@ -449,32 +502,24 @@ function GroupLeaderboards(props: {
}) { }) {
const { traderScores, creatorScores, members, topTraders, topCreators } = const { traderScores, creatorScores, members, topTraders, topCreators } =
props props
const [includeOutsiders, setIncludeOutsiders] = useState(false)
// Consider hiding M$0 // Consider hiding M$0
// If it's just one member (curator), show all bettors, otherwise just show members
return ( return (
<Col> <Col>
<Row className="items-center justify-end gap-4 text-gray-500">
Include all users
<ShortToggle
enabled={includeOutsiders}
setEnabled={setIncludeOutsiders}
/>
</Row>
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
{!includeOutsiders ? ( {members.length > 1 ? (
<> <>
<SortedLeaderboard <SortedLeaderboard
users={members} users={members}
scoreFunction={(user) => traderScores[user.id] ?? 0} scoreFunction={(user) => traderScores[user.id] ?? 0}
title="🏅 Top bettors" title="🏅 Bettor rankings"
header="Profit" header="Profit"
/> />
<SortedLeaderboard <SortedLeaderboard
users={members} users={members}
scoreFunction={(user) => creatorScores[user.id] ?? 0} scoreFunction={(user) => creatorScores[user.id] ?? 0}
title="🏅 Top creators" title="🏅 Creator rankings"
header="Market volume" header="Market volume"
/> />
</> </>
@ -543,16 +588,17 @@ function AddContractButton(props: { group: Group; user: User }) {
</div> </div>
</Col> </Col>
</Modal> </Modal>
<Row className={'items-center justify-center'}> <div className={'flex w-48 justify-center'}>
<button <button
className={ className={clsx(
'btn btn-md btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case' createButtonStyle,
} 'w-48 whitespace-nowrap border border-black text-black hover:bg-black hover:text-white'
)}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
Add an old question Add an old question
</button> </button>
</Row> </div>
</> </>
) )
} }

View File

@ -347,10 +347,10 @@ function IncomeNotificationItem(props: {
let reasonText = '' let reasonText = ''
if (sourceType === 'bonus' && sourceText) { if (sourceType === 'bonus' && sourceText) {
reasonText = !simple reasonText = !simple
? `bonus for ${ ? `Bonus for ${
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
} unique bettors` } unique bettors`
: ' bonus for unique bettors on' : 'bonus on'
} else if (sourceType === 'tip') { } else if (sourceType === 'tip') {
reasonText = !simple ? `tipped you` : `in tips on` reasonText = !simple ? `tipped you` : `in tips on`
} }