Small groups UX changes
This commit is contained in:
parent
2591655269
commit
a23c744c3e
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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}` : ''}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ? (
|
||||||
<CreateQuestionButton
|
<Row className={'justify-between sm:flex-col sm:justify-center'}>
|
||||||
user={user}
|
<CreateQuestionButton
|
||||||
overrideText={'Add a new question'}
|
user={user}
|
||||||
className={'w-48 flex-shrink-0'}
|
overrideText={'Add a new question'}
|
||||||
query={`?groupId=${group.id}`}
|
className={'w-48 flex-shrink-0'}
|
||||||
/>
|
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,11 +402,51 @@ 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function GroupMembersList(props: { group: Group }) {
|
export function GroupMembersList(props: { group: Group }) {
|
||||||
const { group } = props
|
const { group } = props
|
||||||
const members = useMembers(group)
|
const members = useMembers(group)
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user