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:
Ian Philips 2022-07-22 11:34:10 -06:00 committed by GitHub
parent 163c990e9d
commit 6fb9849007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 66 deletions

View File

@ -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}

View File

@ -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>
))}
</>
)

View 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>
)
}

View File

@ -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

View File

@ -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
}

View File

@ -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, {

View File

@ -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}

View File

@ -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>