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
|
||||
children?: ReactNode
|
||||
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'
|
||||
disabled?: boolean
|
||||
}) {
|
||||
|
@ -40,6 +40,7 @@ export function Button(props: {
|
|||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
|
||||
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-white' && 'bg-white text-gray-500 hover:bg-gray-200',
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -26,8 +26,6 @@ import NewContractBadge from '../new-contract-badge'
|
|||
import { CATEGORY_LIST } from 'common/categories'
|
||||
import { TagsList } from '../tags-list'
|
||||
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 { useGroupsWithContract } from 'web/hooks/use-group'
|
||||
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 { exhibitExts } from 'common/util/parse'
|
||||
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'
|
||||
|
||||
|
@ -135,31 +137,11 @@ export function ContractDetails(props: {
|
|||
const { closeTime, creatorName, creatorUsername, creatorId } = contract
|
||||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||
|
||||
const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => {
|
||||
return g2.createdTime - g1.createdTime
|
||||
})
|
||||
const groups = useGroupsWithContract(contract)
|
||||
const groupToDisplay = groups[0] ?? null
|
||||
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 (
|
||||
<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">
|
||||
|
@ -180,16 +162,34 @@ export function ContractDetails(props: {
|
|||
)}
|
||||
{!disabled && <UserFollowButton userId={creatorId} small />}
|
||||
</Row>
|
||||
{groupToDisplay ? (
|
||||
<Row className={'line-clamp-1 mt-1 max-w-[200px]'}>
|
||||
<SiteLink href={`${groupPath(groupToDisplay.slug)}`}>
|
||||
<UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" />
|
||||
<span>{groupToDisplay.name}</span>
|
||||
</SiteLink>
|
||||
</Row>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Row>
|
||||
<Button
|
||||
size={'xs'}
|
||||
className={'max-w-[200px]'}
|
||||
color={'gray-white'}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<Row>
|
||||
<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) && (
|
||||
<Row className="items-center gap-1">
|
||||
|
@ -326,12 +326,13 @@ function EditableCloseDate(props: {
|
|||
Done
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-xs btn-ghost"
|
||||
<Button
|
||||
size={'xs'}
|
||||
color={'gray-white'}
|
||||
onClick={() => setIsEditingCloseTime(true)}
|
||||
>
|
||||
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
||||
</button>
|
||||
<PencilIcon className="mr-0.5 inline h-4 w-4" /> Edit
|
||||
</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'
|
||||
|
||||
export function GroupSelector(props: {
|
||||
selectedGroup?: Group
|
||||
selectedGroup: Group | undefined
|
||||
setSelectedGroup: (group: Group) => void
|
||||
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 { showSelector, showLabel, ignoreGroupIds } = options
|
||||
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) =>
|
||||
searchInAny(query, group.name)
|
||||
)
|
||||
|
@ -55,16 +61,18 @@ export function GroupSelector(props: {
|
|||
>
|
||||
{() => (
|
||||
<>
|
||||
<Combobox.Label className="label justify-start gap-2 text-base">
|
||||
Add to Group
|
||||
<InfoTooltip text="Question will be displayed alongside the other questions in the group." />
|
||||
</Combobox.Label>
|
||||
{showLabel && (
|
||||
<Combobox.Label className="label justify-start gap-2 text-base">
|
||||
Add to Group
|
||||
<InfoTooltip text="Question will be displayed alongside the other questions in the group." />
|
||||
</Combobox.Label>
|
||||
)}
|
||||
<div className="relative mt-2">
|
||||
<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)}
|
||||
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">
|
||||
<SelectorIcon
|
||||
|
|
|
@ -2,13 +2,15 @@ import { useEffect, useState } from 'react'
|
|||
import { Group } from 'common/group'
|
||||
import { User } from 'common/user'
|
||||
import {
|
||||
getGroupsWithContractId,
|
||||
listenForGroup,
|
||||
listenForGroups,
|
||||
listenForMemberGroups,
|
||||
listGroups,
|
||||
} from 'web/lib/firebase/groups'
|
||||
import { getUser, getUsers } from 'web/lib/firebase/users'
|
||||
import { filterDefined } from 'common/util/array'
|
||||
import { Contract } from 'common/contract'
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
export const useGroup = (groupId: string | 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))
|
||||
}
|
||||
|
||||
export const useGroupsWithContract = (contractId: string | undefined) => {
|
||||
const [groups, setGroups] = useState<Group[] | null | undefined>()
|
||||
export const useGroupsWithContract = (contract: Contract) => {
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (contractId) getGroupsWithContractId(contractId, setGroups)
|
||||
}, [contractId])
|
||||
if (contract.groupSlugs)
|
||||
listGroups(uniq(contract.groupSlugs)).then((groups) =>
|
||||
setGroups(filterDefined(groups))
|
||||
)
|
||||
}, [contract.groupSlugs])
|
||||
|
||||
return groups
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@ export async function listAllGroups() {
|
|||
return getValues<Group>(groups)
|
||||
}
|
||||
|
||||
export async function listGroups(groupSlugs: string[]) {
|
||||
return Promise.all(groupSlugs.map(getGroupBySlug))
|
||||
}
|
||||
|
||||
export function listenForGroups(setGroups: (groups: Group[]) => void) {
|
||||
return listenForValues(groups, setGroups)
|
||||
}
|
||||
|
@ -86,12 +90,12 @@ export function listenForMemberGroups(
|
|||
})
|
||||
}
|
||||
|
||||
export async function getGroupsWithContractId(
|
||||
export async function listenForGroupsWithContractId(
|
||||
contractId: string,
|
||||
setGroups: (groups: Group[]) => void
|
||||
) {
|
||||
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) {
|
||||
|
@ -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) {
|
||||
await updateContract(contractId, { groupSlugs: [group.slug] })
|
||||
return await updateGroup(group, {
|
||||
|
|
|
@ -20,7 +20,7 @@ import Custom404 from '../404'
|
|||
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
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 { SEO } from 'web/components/SEO'
|
||||
|
||||
|
@ -65,11 +65,7 @@ function CharityPage(props: { charity: Charity }) {
|
|||
/>
|
||||
}
|
||||
>
|
||||
<SEO
|
||||
title={name}
|
||||
description={description}
|
||||
url="/groups"
|
||||
/>
|
||||
<SEO title={name} description={description} url="/groups" />
|
||||
{showConfetti && (
|
||||
<Confetti
|
||||
width={width ? width : 500}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { formatMoney } from 'common/util/format'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
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 { useTracking } from 'web/hooks/use-tracking'
|
||||
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
||||
|
@ -353,7 +353,7 @@ export function NewContract(props: {
|
|||
selectedGroup={selectedGroup}
|
||||
setSelectedGroup={setSelectedGroup}
|
||||
creator={creator}
|
||||
showSelector={showGroupSelector}
|
||||
options={{ showSelector: showGroupSelector, showLabel: true }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user