Allow to add/remove from groups on market page (#685)
* Allow to add/remove from groups on market page * remove lib * Fix Sinclair's relative import from May * Clean
This commit is contained in:
parent
163c990e9d
commit
6fb9849007
|
@ -6,7 +6,7 @@ export function Button(props: {
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray'
|
color?: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray' | 'gray-white'
|
||||||
type?: 'button' | 'reset' | 'submit'
|
type?: 'button' | 'reset' | 'submit'
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -40,6 +40,7 @@ export function Button(props: {
|
||||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||||
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
|
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
|
||||||
color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
color === 'gray' && 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
||||||
|
color === 'gray-white' && 'bg-white text-gray-500 hover:bg-gray-200',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -26,8 +26,6 @@ import NewContractBadge from '../new-contract-badge'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { TagsList } from '../tags-list'
|
import { TagsList } from '../tags-list'
|
||||||
import { UserFollowButton } from '../follow-button'
|
import { UserFollowButton } from '../follow-button'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
|
||||||
import { SiteLink } from 'web/components/site-link'
|
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { useGroupsWithContract } from 'web/hooks/use-group'
|
import { useGroupsWithContract } from 'web/hooks/use-group'
|
||||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
|
@ -35,6 +33,10 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { Editor } from '@tiptap/react'
|
import { Editor } from '@tiptap/react'
|
||||||
import { exhibitExts } from 'common/util/parse'
|
import { exhibitExts } from 'common/util/parse'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -135,31 +137,11 @@ export function ContractDetails(props: {
|
||||||
const { closeTime, creatorName, creatorUsername, creatorId } = contract
|
const { closeTime, creatorName, creatorUsername, creatorId } = contract
|
||||||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => {
|
const groups = useGroupsWithContract(contract)
|
||||||
return g2.createdTime - g1.createdTime
|
const groupToDisplay = groups[0] ?? null
|
||||||
})
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const groupsUserIsMemberOf = groups
|
|
||||||
? groups.filter((g) => g.memberIds.includes(contract.creatorId))
|
|
||||||
: []
|
|
||||||
const groupsUserIsCreatorOf = groups
|
|
||||||
? groups.filter((g) => g.creatorId === contract.creatorId)
|
|
||||||
: []
|
|
||||||
|
|
||||||
// Priorities for which group the contract belongs to:
|
|
||||||
// In order of created most recently
|
|
||||||
// Group that the contract owner created
|
|
||||||
// Group the contract owner is a member of
|
|
||||||
// Any group the contract is in
|
|
||||||
const groupToDisplay =
|
|
||||||
groupsUserIsCreatorOf.length > 0
|
|
||||||
? groupsUserIsCreatorOf[0]
|
|
||||||
: groupsUserIsMemberOf.length > 0
|
|
||||||
? groupsUserIsMemberOf[0]
|
|
||||||
: groups
|
|
||||||
? groups[0]
|
|
||||||
: undefined
|
|
||||||
return (
|
return (
|
||||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -180,16 +162,34 @@ export function ContractDetails(props: {
|
||||||
)}
|
)}
|
||||||
{!disabled && <UserFollowButton userId={creatorId} small />}
|
{!disabled && <UserFollowButton userId={creatorId} small />}
|
||||||
</Row>
|
</Row>
|
||||||
{groupToDisplay ? (
|
<Row>
|
||||||
<Row className={'line-clamp-1 mt-1 max-w-[200px]'}>
|
<Button
|
||||||
<SiteLink href={`${groupPath(groupToDisplay.slug)}`}>
|
size={'xs'}
|
||||||
<UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" />
|
className={'max-w-[200px]'}
|
||||||
<span>{groupToDisplay.name}</span>
|
color={'gray-white'}
|
||||||
</SiteLink>
|
onClick={() => setOpen(!open)}
|
||||||
</Row>
|
>
|
||||||
) : (
|
<Row>
|
||||||
<div />
|
<UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" />
|
||||||
)}
|
<span className={'line-clamp-1'}>
|
||||||
|
{contract.groupSlugs && !groupToDisplay
|
||||||
|
? ''
|
||||||
|
: groupToDisplay
|
||||||
|
? groupToDisplay.name
|
||||||
|
: 'No group'}
|
||||||
|
</span>
|
||||||
|
</Row>
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<Modal open={open} setOpen={setOpen} size={'md'}>
|
||||||
|
<Col
|
||||||
|
className={
|
||||||
|
'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ContractGroupsList groups={groups} contract={contract} user={user} />
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{(!!closeTime || !!resolvedDate) && (
|
{(!!closeTime || !!resolvedDate) && (
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
|
@ -326,12 +326,13 @@ function EditableCloseDate(props: {
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
className="btn btn-xs btn-ghost"
|
size={'xs'}
|
||||||
|
color={'gray-white'}
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
onClick={() => setIsEditingCloseTime(true)}
|
||||||
>
|
>
|
||||||
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
<PencilIcon className="mr-0.5 inline h-4 w-4" /> Edit
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
66
web/components/groups/contract-groups-list.tsx
Normal file
66
web/components/groups/contract-groups-list.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { Group } from 'common/group'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { GroupLink } from 'web/pages/groups'
|
||||||
|
import { XIcon } from '@heroicons/react/outline'
|
||||||
|
import { Button } from 'web/components/button'
|
||||||
|
import { GroupSelector } from 'web/components/groups/group-selector'
|
||||||
|
import {
|
||||||
|
addContractToGroup,
|
||||||
|
removeContractFromGroup,
|
||||||
|
} from 'web/lib/firebase/groups'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
|
||||||
|
export function ContractGroupsList(props: {
|
||||||
|
groups: Group[]
|
||||||
|
contract: Contract
|
||||||
|
user: User | null | undefined
|
||||||
|
}) {
|
||||||
|
const { groups, user, contract } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={'gap-2'}>
|
||||||
|
{user && (
|
||||||
|
<Row className={'ml-2 items-center justify-between'}>
|
||||||
|
<span>Add to group: </span>
|
||||||
|
<GroupSelector
|
||||||
|
options={{
|
||||||
|
showSelector: true,
|
||||||
|
showLabel: false,
|
||||||
|
ignoreGroupIds: groups.map((g) => g.id),
|
||||||
|
}}
|
||||||
|
setSelectedGroup={(group) => addContractToGroup(group, contract)}
|
||||||
|
selectedGroup={undefined}
|
||||||
|
creator={user}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{groups.length === 0 && (
|
||||||
|
<Col className="ml-2 h-full justify-center text-gray-500">
|
||||||
|
No groups yet...
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{groups.map((group) => (
|
||||||
|
<Row
|
||||||
|
key={group.id}
|
||||||
|
className={clsx('items-center justify-between gap-2 p-2')}
|
||||||
|
>
|
||||||
|
<Row className="line-clamp-1 items-center gap-2">
|
||||||
|
<GroupLink group={group} />
|
||||||
|
</Row>
|
||||||
|
{user && group.memberIds.includes(user.id) && (
|
||||||
|
<Button
|
||||||
|
color={'gray-white'}
|
||||||
|
size={'xs'}
|
||||||
|
onClick={() => removeContractFromGroup(group, contract)}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4 text-gray-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
|
@ -14,16 +14,22 @@ import { User } from 'common/user'
|
||||||
import { searchInAny } from 'common/util/parse'
|
import { searchInAny } from 'common/util/parse'
|
||||||
|
|
||||||
export function GroupSelector(props: {
|
export function GroupSelector(props: {
|
||||||
selectedGroup?: Group
|
selectedGroup: Group | undefined
|
||||||
setSelectedGroup: (group: Group) => void
|
setSelectedGroup: (group: Group) => void
|
||||||
creator: User | null | undefined
|
creator: User | null | undefined
|
||||||
showSelector?: boolean
|
options: {
|
||||||
|
showSelector: boolean
|
||||||
|
showLabel: boolean
|
||||||
|
ignoreGroupIds?: string[]
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
const { selectedGroup, setSelectedGroup, creator, showSelector } = props
|
const { selectedGroup, setSelectedGroup, creator, options } = props
|
||||||
const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false)
|
const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false)
|
||||||
|
const { showSelector, showLabel, ignoreGroupIds } = options
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const memberGroups = useMemberGroups(creator?.id) ?? []
|
const memberGroups = (useMemberGroups(creator?.id) ?? []).filter(
|
||||||
|
(group) => !ignoreGroupIds?.includes(group.id)
|
||||||
|
)
|
||||||
const filteredGroups = memberGroups.filter((group) =>
|
const filteredGroups = memberGroups.filter((group) =>
|
||||||
searchInAny(query, group.name)
|
searchInAny(query, group.name)
|
||||||
)
|
)
|
||||||
|
@ -55,16 +61,18 @@ export function GroupSelector(props: {
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<Combobox.Label className="label justify-start gap-2 text-base">
|
{showLabel && (
|
||||||
Add to Group
|
<Combobox.Label className="label justify-start gap-2 text-base">
|
||||||
<InfoTooltip text="Question will be displayed alongside the other questions in the group." />
|
Add to Group
|
||||||
</Combobox.Label>
|
<InfoTooltip text="Question will be displayed alongside the other questions in the group." />
|
||||||
|
</Combobox.Label>
|
||||||
|
)}
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-2">
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-4 pr-20 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 "
|
className="w-60 rounded-md border border-gray-300 bg-white p-3 pl-4 pr-20 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 "
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
displayValue={(group: Group) => group && group.name}
|
displayValue={(group: Group) => group && group.name}
|
||||||
placeholder={'None'}
|
placeholder={'E.g. Science, Politics'}
|
||||||
/>
|
/>
|
||||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
|
||||||
<SelectorIcon
|
<SelectorIcon
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { useEffect, useState } from 'react'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
getGroupsWithContractId,
|
|
||||||
listenForGroup,
|
listenForGroup,
|
||||||
listenForGroups,
|
listenForGroups,
|
||||||
listenForMemberGroups,
|
listenForMemberGroups,
|
||||||
|
listGroups,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { getUser, getUsers } from 'web/lib/firebase/users'
|
import { getUser, getUsers } from 'web/lib/firebase/users'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
export const useGroup = (groupId: string | undefined) => {
|
export const useGroup = (groupId: string | undefined) => {
|
||||||
const [group, setGroup] = useState<Group | null | undefined>()
|
const [group, setGroup] = useState<Group | null | undefined>()
|
||||||
|
@ -103,12 +105,15 @@ export async function listMembers(group: Group, max?: number) {
|
||||||
return await Promise.all(group.memberIds.slice(0, numToRetrieve).map(getUser))
|
return await Promise.all(group.memberIds.slice(0, numToRetrieve).map(getUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGroupsWithContract = (contractId: string | undefined) => {
|
export const useGroupsWithContract = (contract: Contract) => {
|
||||||
const [groups, setGroups] = useState<Group[] | null | undefined>()
|
const [groups, setGroups] = useState<Group[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contractId) getGroupsWithContractId(contractId, setGroups)
|
if (contract.groupSlugs)
|
||||||
}, [contractId])
|
listGroups(uniq(contract.groupSlugs)).then((groups) =>
|
||||||
|
setGroups(filterDefined(groups))
|
||||||
|
)
|
||||||
|
}, [contract.groupSlugs])
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,10 @@ export async function listAllGroups() {
|
||||||
return getValues<Group>(groups)
|
return getValues<Group>(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listGroups(groupSlugs: string[]) {
|
||||||
|
return Promise.all(groupSlugs.map(getGroupBySlug))
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForGroups(setGroups: (groups: Group[]) => void) {
|
export function listenForGroups(setGroups: (groups: Group[]) => void) {
|
||||||
return listenForValues(groups, setGroups)
|
return listenForValues(groups, setGroups)
|
||||||
}
|
}
|
||||||
|
@ -86,12 +90,12 @@ export function listenForMemberGroups(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupsWithContractId(
|
export async function listenForGroupsWithContractId(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setGroups: (groups: Group[]) => void
|
setGroups: (groups: Group[]) => void
|
||||||
) {
|
) {
|
||||||
const q = query(groups, where('contractIds', 'array-contains', contractId))
|
const q = query(groups, where('contractIds', 'array-contains', contractId))
|
||||||
setGroups(await getValues<Group>(q))
|
return listenForValues<Group>(q, setGroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addUserToGroupViaId(groupId: string, userId: string) {
|
export async function addUserToGroupViaId(groupId: string, userId: string) {
|
||||||
|
@ -134,6 +138,27 @@ export async function addContractToGroup(group: Group, contract: Contract) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeContractFromGroup(
|
||||||
|
group: Group,
|
||||||
|
contract: Contract
|
||||||
|
) {
|
||||||
|
const newGroupSlugs = contract.groupSlugs?.filter(
|
||||||
|
(slug) => slug !== group.slug
|
||||||
|
)
|
||||||
|
await updateContract(contract.id, {
|
||||||
|
groupSlugs: uniq(newGroupSlugs ?? []),
|
||||||
|
})
|
||||||
|
const newContractIds = group.contractIds.filter((id) => id !== contract.id)
|
||||||
|
return await updateGroup(group, {
|
||||||
|
contractIds: uniq(newContractIds),
|
||||||
|
})
|
||||||
|
.then(() => group)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('error removing contract from group', err)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function setContractGroupSlugs(group: Group, contractId: string) {
|
export async function setContractGroupSlugs(group: Group, contractId: string) {
|
||||||
await updateContract(contractId, { groupSlugs: [group.slug] })
|
await updateContract(contractId, { groupSlugs: [group.slug] })
|
||||||
return await updateGroup(group, {
|
return await updateGroup(group, {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import Custom404 from '../404'
|
||||||
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import { Donation } from 'web/components/charity/feed-items'
|
import { Donation } from 'web/components/charity/feed-items'
|
||||||
import { manaToUSD } from '../../../common/util/format'
|
import { manaToUSD } from 'common/util/format'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
|
||||||
|
@ -65,11 +65,7 @@ function CharityPage(props: { charity: Charity }) {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SEO
|
<SEO title={name} description={description} url="/groups" />
|
||||||
title={name}
|
|
||||||
description={description}
|
|
||||||
url="/groups"
|
|
||||||
/>
|
|
||||||
{showConfetti && (
|
{showConfetti && (
|
||||||
<Confetti
|
<Confetti
|
||||||
width={width ? width : 500}
|
width={width ? width : 500}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { setContractGroupSlugs, getGroup } from 'web/lib/firebase/groups'
|
import { getGroup, setContractGroupSlugs } from 'web/lib/firebase/groups'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
||||||
|
@ -353,7 +353,7 @@ export function NewContract(props: {
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
setSelectedGroup={setSelectedGroup}
|
setSelectedGroup={setSelectedGroup}
|
||||||
creator={creator}
|
creator={creator}
|
||||||
showSelector={showGroupSelector}
|
options={{ showSelector: showGroupSelector, showLabel: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user