From 6fb984900787234ef4062388e7e2b759b854819f Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Fri, 22 Jul 2022 11:34:10 -0600 Subject: [PATCH] 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 --- web/components/button.tsx | 3 +- web/components/contract/contract-details.tsx | 79 ++++++++++--------- .../groups/contract-groups-list.tsx | 66 ++++++++++++++++ web/components/groups/group-selector.tsx | 30 ++++--- web/hooks/use-group.ts | 15 ++-- web/lib/firebase/groups.ts | 29 ++++++- web/pages/charity/[charitySlug].tsx | 8 +- web/pages/create.tsx | 4 +- 8 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 web/components/groups/contract-groups-list.tsx diff --git a/web/components/button.tsx b/web/components/button.tsx index d279d9a0..8877c023 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -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} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 036311fe..0f5a1d42 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -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 ( @@ -180,16 +162,34 @@ export function ContractDetails(props: { )} {!disabled && } - {groupToDisplay ? ( - - - - {groupToDisplay.name} - - - ) : ( -
- )} + + + + + + + + {(!!closeTime || !!resolvedDate) && ( @@ -326,12 +326,13 @@ function EditableCloseDate(props: { Done ) : ( - + Edit + ))} ) diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx new file mode 100644 index 00000000..b52179b1 --- /dev/null +++ b/web/components/groups/contract-groups-list.tsx @@ -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 ( + + {user && ( + + Add to group: + g.id), + }} + setSelectedGroup={(group) => addContractToGroup(group, contract)} + selectedGroup={undefined} + creator={user} + /> + + )} + {groups.length === 0 && ( + + No groups yet... + + )} + {groups.map((group) => ( + + + + + {user && group.memberIds.includes(user.id) && ( + + )} + + ))} + + ) +} diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index c7b4cb39..e6270a4d 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -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: { > {() => ( <> - - Add to Group - - + {showLabel && ( + + Add to Group + + + )}
setQuery(event.target.value)} displayValue={(group: Group) => group && group.name} - placeholder={'None'} + placeholder={'E.g. Science, Politics'} /> { const [group, setGroup] = useState() @@ -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() +export const useGroupsWithContract = (contract: Contract) => { + const [groups, setGroups] = useState([]) useEffect(() => { - if (contractId) getGroupsWithContractId(contractId, setGroups) - }, [contractId]) + if (contract.groupSlugs) + listGroups(uniq(contract.groupSlugs)).then((groups) => + setGroups(filterDefined(groups)) + ) + }, [contract.groupSlugs]) return groups } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 5a031ca7..dc096f4e 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -44,6 +44,10 @@ export async function listAllGroups() { return getValues(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(q)) + return listenForValues(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, { diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index da3141d2..89d2d3a3 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -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 }) { /> } > - + {showConfetti && (