Allow adding anyone's contract to a group

This commit is contained in:
Ian Philips 2022-07-01 16:37:30 -06:00
parent cb68530e2a
commit b9931e65da
15 changed files with 150 additions and 107 deletions

View File

@ -21,11 +21,16 @@ service cloud.firestore {
allow update: if resource.data.id == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']); .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
// only one referral allowed per user
allow update: if resource.data.id == request.auth.uid allow update: if resource.data.id == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId']) .hasOnly(['referredByUserId'])
&& !("referredByUserId" in resource.data); // only one referral allowed per user
&& !("referredByUserId" in resource.data)
// user can't refer themselves
&& (resource.data.id != request.resource.data.referredByUserId)
// user can't refer someone who referred them quid pro quo
&& get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId != resource.data.id;
} }
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} { match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {

View File

@ -9,7 +9,7 @@ import {
useSortBy, useSortBy,
} from 'react-instantsearch-hooks-web' } from 'react-instantsearch-hooks-web'
import { Contract } from '../../common/contract' import { Contract } from 'common/contract'
import { import {
Sort, Sort,
useInitialQueryAndSort, useInitialQueryAndSort,
@ -58,15 +58,24 @@ export function ContractSearch(props: {
additionalFilter?: { additionalFilter?: {
creatorId?: string creatorId?: string
tag?: string tag?: string
excludeContractIds?: string[]
} }
showCategorySelector: boolean showCategorySelector: boolean
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
showPlaceHolder?: boolean
hideOrderSelector?: boolean
overrideGridClassName?: string
hideQuickBet?: boolean
}) { }) {
const { const {
querySortOptions, querySortOptions,
additionalFilter, additionalFilter,
showCategorySelector, showCategorySelector,
onContractClick, onContractClick,
overrideGridClassName,
hideOrderSelector,
showPlaceHolder,
hideQuickBet,
} = props } = props
const user = useUser() const user = useUser()
@ -136,6 +145,7 @@ export function ContractSearch(props: {
<Row className="gap-1 sm:gap-2"> <Row className="gap-1 sm:gap-2">
<SearchBox <SearchBox
className="flex-1" className="flex-1"
placeholder={showPlaceHolder ? `Search ${filter} contracts` : ''}
classNames={{ classNames={{
form: 'before:top-6', form: 'before:top-6',
input: '!pl-10 !input !input-bordered shadow-none w-[100px]', input: '!pl-10 !input !input-bordered shadow-none w-[100px]',
@ -153,13 +163,15 @@ export function ContractSearch(props: {
<option value="resolved">Resolved</option> <option value="resolved">Resolved</option>
<option value="all">All</option> <option value="all">All</option>
</select> </select>
<SortBy {!hideOrderSelector && (
items={sortIndexes} <SortBy
classNames={{ items={sortIndexes}
select: '!select !select-bordered', classNames={{
}} select: '!select !select-bordered',
onBlur={trackCallback('select search sort')} }}
/> onBlur={trackCallback('select search sort')}
/>
)}
<Configure <Configure
facetFilters={filters} facetFilters={filters}
numericFilters={numericFilters} numericFilters={numericFilters}
@ -187,6 +199,9 @@ export function ContractSearch(props: {
<ContractSearchInner <ContractSearchInner
querySortOptions={querySortOptions} querySortOptions={querySortOptions}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName}
hideQuickBet={hideQuickBet}
excludeContractIds={additionalFilter?.excludeContractIds}
/> />
)} )}
</InstantSearch> </InstantSearch>
@ -199,8 +214,17 @@ export function ContractSearchInner(props: {
shouldLoadFromStorage?: boolean shouldLoadFromStorage?: boolean
} }
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
overrideGridClassName?: string
hideQuickBet?: boolean
excludeContractIds?: string[]
}) { }) {
const { querySortOptions, onContractClick } = props const {
querySortOptions,
onContractClick,
overrideGridClassName,
hideQuickBet,
excludeContractIds,
} = props
const { initialQuery } = useInitialQueryAndSort(querySortOptions) const { initialQuery } = useInitialQueryAndSort(querySortOptions)
const { query, setQuery, setSort } = useUpdateQueryAndSort({ const { query, setQuery, setSort } = useUpdateQueryAndSort({
@ -239,7 +263,7 @@ export function ContractSearchInner(props: {
}, []) }, [])
const { showMore, hits, isLastPage } = useInfiniteHits() const { showMore, hits, isLastPage } = useInfiniteHits()
const contracts = hits as any as Contract[] let contracts = hits as any as Contract[]
if (isInitialLoad && contracts.length === 0) return <></> if (isInitialLoad && contracts.length === 0) return <></>
@ -249,6 +273,9 @@ export function ContractSearchInner(props: {
? 'resolve-date' ? 'resolve-date'
: undefined : undefined
if (excludeContractIds)
contracts = contracts.filter((c) => !excludeContractIds.includes(c.id))
return ( return (
<ContractsGrid <ContractsGrid
contracts={contracts} contracts={contracts}
@ -256,6 +283,8 @@ export function ContractSearchInner(props: {
hasMore={!isLastPage} hasMore={!isLastPage}
showTime={showTime} showTime={showTime}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName}
hideQuickBet={hideQuickBet}
/> />
) )
} }

View File

@ -130,9 +130,32 @@ export function ContractDetails(props: {
const { contract, bets, isCreator, disabled } = props const { contract, bets, isCreator, disabled } = props
const { closeTime, creatorName, creatorUsername, creatorId } = contract const { closeTime, creatorName, creatorUsername, creatorId } = contract
const { volumeLabel, resolvedDate } = contractMetrics(contract) const { volumeLabel, resolvedDate } = contractMetrics(contract)
// Find a group that this contract id is in
const groups = useGroupsWithContract(contract.id) const groups = (useGroupsWithContract(contract.id) ?? []).sort((g1, g2) => {
return g2.createdTime - g1.createdTime
})
const user = useUser() const user = useUser()
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">
@ -153,14 +176,15 @@ export function ContractDetails(props: {
)} )}
{!disabled && <UserFollowButton userId={creatorId} small />} {!disabled && <UserFollowButton userId={creatorId} small />}
</Row> </Row>
{/*// TODO: we can add contracts to multiple groups but only show the first it was added to*/} {groupToDisplay ? (
{groups && groups.length > 0 && (
<Row className={'line-clamp-1 mt-1 max-w-[200px]'}> <Row className={'line-clamp-1 mt-1 max-w-[200px]'}>
<SiteLink href={`${groupPath(groups[0].slug)}`}> <SiteLink href={`${groupPath(groupToDisplay.slug)}`}>
<UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" /> <UserGroupIcon className="mx-1 mb-1 inline h-5 w-5" />
<span>{groups[0].name}</span> <span>{groupToDisplay.name}</span>
</SiteLink> </SiteLink>
</Row> </Row>
) : (
<div />
)} )}
{(!!closeTime || !!resolvedDate) && ( {(!!closeTime || !!resolvedDate) && (

View File

@ -9,6 +9,7 @@ import { useRouter } from 'next/router'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { FilterSelectUsers } from 'web/components/filter-select-users' import { FilterSelectUsers } from 'web/components/filter-select-users'
import { User } from 'common/user' import { User } from 'common/user'
import { uniq } from 'lodash'
export function EditGroupButton(props: { group: Group; className?: string }) { export function EditGroupButton(props: { group: Group; className?: string }) {
const { group, className } = props const { group, className } = props
@ -35,7 +36,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
await updateGroup(group, { await updateGroup(group, {
name, name,
about, about,
memberIds: [...memberIds, ...addMemberUsers.map((user) => user.id)], memberIds: uniq([...memberIds, ...addMemberUsers.map((user) => user.id)]),
}) })
setIsSubmitting(false) setIsSubmitting(false)

View File

@ -9,7 +9,7 @@ import { TextButton } from 'web/components/text-button'
import { Group } from 'common/group' import { Group } from 'common/group'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { addUserToGroup, leaveGroup } from 'web/lib/firebase/groups'
import { firebaseLogin } from 'web/lib/firebase/users' import { firebaseLogin } from 'web/lib/firebase/users'
import { GroupLink } from 'web/pages/groups' import { GroupLink } from 'web/pages/groups'
@ -93,7 +93,7 @@ export function JoinOrLeaveGroupButton(props: {
: false : false
const onJoinGroup = () => { const onJoinGroup = () => {
if (!currentUser) return if (!currentUser) return
joinGroup(group, currentUser.id) addUserToGroup(group, currentUser.id)
} }
const onLeaveGroup = () => { const onLeaveGroup = () => {
if (!currentUser) return if (!currentUser) return

View File

@ -1,13 +1,15 @@
import { Fragment, ReactNode } from 'react' import { Fragment, ReactNode } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import clsx from 'clsx'
// From https://tailwindui.com/components/application-ui/overlays/modals // From https://tailwindui.com/components/application-ui/overlays/modals
export function Modal(props: { export function Modal(props: {
children: ReactNode children: ReactNode
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
className?: string
}) { }) {
const { children, open, setOpen } = props const { children, open, setOpen, className } = props
return ( return (
<Transition.Root show={open} as={Fragment}> <Transition.Root show={open} as={Fragment}>
@ -45,7 +47,12 @@ export function Modal(props: {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<div className="inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle"> <div
className={clsx(
'inline-block transform overflow-hidden text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-md sm:p-6 sm:align-middle',
className
)}
>
{children} {children}
</div> </div>
</Transition.Child> </Transition.Child>

View File

@ -14,17 +14,17 @@ type Tab = {
export function Tabs(props: { export function Tabs(props: {
tabs: Tab[] tabs: Tab[]
defaultIndex?: number defaultIndex?: number
className?: string labelClassName?: string
onClick?: (tabTitle: string, index: number) => void onClick?: (tabTitle: string, index: number) => void
}) { }) {
const { tabs, defaultIndex, className, onClick } = props const { tabs, defaultIndex, labelClassName, onClick } = props
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0) const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case
return ( return (
<div> <>
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> <nav className="-mb-px mb-4 flex space-x-8" aria-label="Tabs">
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> <Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}>
<a <a
@ -42,7 +42,7 @@ export function Tabs(props: {
? 'border-indigo-500 text-indigo-600' ? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium', 'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium',
className labelClassName
)} )}
aria-current={activeIndex === i ? 'page' : undefined} aria-current={activeIndex === i ? 'page' : undefined}
> >
@ -56,7 +56,7 @@ export function Tabs(props: {
</nav> </nav>
</div> </div>
<div className="mt-4">{activeTab?.content}</div> {activeTab?.content}
</div> </>
) )
} }

View File

@ -254,7 +254,7 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
<div className="mt-1 space-y-0.5"> <div className="mt-1 space-y-0.5">
{memberItems.map((item) => ( {memberItems.map((item) => (
<a <a
key={item.name} key={item.href}
href={item.href} href={item.href}
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900" className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900"
> >

View File

@ -258,7 +258,7 @@ export function UserPage(props: {
{usersContracts !== 'loading' && commentsByContract != 'loading' ? ( {usersContracts !== 'loading' && commentsByContract != 'loading' ? (
<Tabs <Tabs
className={'pb-2 pt-1 '} labelClassName={'pb-2 pt-1 '}
defaultIndex={ defaultIndex={
defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0 defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0
} }

View File

@ -74,9 +74,7 @@ export function useMembers(group: Group) {
} }
export async function listMembers(group: Group) { export async function listMembers(group: Group) {
return (await Promise.all(group.memberIds.map(getUser))).filter( return await Promise.all(group.memberIds.map(getUser))
(user) => user
)
} }
export const useGroupsWithContract = (contractId: string | undefined) => { export const useGroupsWithContract = (contractId: string | undefined) => {

View File

@ -102,10 +102,13 @@ export async function addUserToGroupViaSlug(groupSlug: string, userId: string) {
console.error(`Group not found: ${groupSlug}`) console.error(`Group not found: ${groupSlug}`)
return return
} }
return await joinGroup(group, userId) return await addUserToGroup(group, userId)
} }
export async function joinGroup(group: Group, userId: string): Promise<Group> { export async function addUserToGroup(
group: Group,
userId: string
): Promise<Group> {
const { memberIds } = group const { memberIds } = group
if (memberIds.includes(userId)) { if (memberIds.includes(userId)) {
return group return group
@ -125,3 +128,14 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> {
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) }) await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
return newGroup return newGroup
} }
export async function addContractToGroup(group: Group, contractId: string) {
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contractId]),
})
.then(() => group)
.catch((err) => {
console.error('error adding contract to group', err)
return err
})
}

View File

@ -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 { getGroup, updateGroup } from 'web/lib/firebase/groups' import { addContractToGroup, getGroup } 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'
@ -186,9 +186,7 @@ export function NewContract(props: {
isFree: false, isFree: false,
}) })
if (result && selectedGroup) { if (result && selectedGroup) {
await updateGroup(selectedGroup, { await addContractToGroup(selectedGroup, result.id)
contractIds: [...selectedGroup.contractIds, result.id],
})
} }
await router.push(contractPath(result as Contract)) await router.push(contractPath(result as Contract))

View File

@ -4,12 +4,14 @@ import { Group } from 'common/group'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { listAllBets } from 'web/lib/firebase/bets' import { listAllBets } from 'web/lib/firebase/bets'
import { Contract, listenForUserContracts } from 'web/lib/firebase/contracts' import { Contract } from 'web/lib/firebase/contracts'
import { import {
groupPath, groupPath,
getGroupBySlug, getGroupBySlug,
getGroupContracts, getGroupContracts,
updateGroup, updateGroup,
addContractToGroup,
addUserToGroup,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page' import { UserLink } from 'web/components/user-page'
@ -39,7 +41,6 @@ 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'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { PlusIcon } from '@heroicons/react/outline'
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' 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'
@ -48,6 +49,7 @@ 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'
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[] } }) {
@ -509,75 +511,46 @@ function GroupLeaderboards(props: {
} }
function AddContractButton(props: { group: Group; user: User }) { function AddContractButton(props: { group: Group; user: User }) {
const { group, user } = props const { group } = props
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [contracts, setContracts] = useState<Contract[] | undefined>(undefined)
const [query, setQuery] = useState('')
useEffect(() => { async function addContractToCurrentGroup(contract: Contract) {
return listenForUserContracts(user.id, (contracts) => { await addContractToGroup(group, contract.id)
setContracts(contracts.filter((c) => !group.contractIds.includes(c.id)))
})
}, [group.contractIds, user.id])
async function addContractToGroup(contract: Contract) {
await updateGroup(group, {
...group,
contractIds: [...group.contractIds, contract.id],
})
setOpen(false) setOpen(false)
} }
// TODO use find-active-contracts to sort by?
const matches = sortBy(contracts, [
(contract) => -1 * contract.createdTime,
]).filter(
(c) =>
checkAgainstQuery(query, c.question) ||
checkAgainstQuery(query, c.description) ||
checkAgainstQuery(query, c.tags.flat().join(' '))
)
const debouncedQuery = debounce(setQuery, 50)
return ( return (
<> <>
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen} className={'sm:p-0'}>
<Col className={'max-h-[60vh] w-full gap-4 rounded-md bg-white p-8'}> <Col
className={
'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white p-8'
}
>
<div className={'text-lg text-indigo-700'}> <div className={'text-lg text-indigo-700'}>
Add a question to your group Add a question to your group
</div> </div>
<input <div className={'overflow-y-scroll p-1'}>
type="text" <ContractSearch
onChange={(e) => debouncedQuery(e.target.value)} hideOrderSelector={true}
placeholder="Search your questions" onContractClick={addContractToCurrentGroup}
className="input input-bordered mb-4 w-full" showCategorySelector={false}
/> overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'}
<div className={'overflow-y-scroll'}> showPlaceHolder={true}
{contracts ? ( hideQuickBet={true}
<ContractsGrid additionalFilter={{ excludeContractIds: group.contractIds }}
contracts={matches} />
loadMore={() => {}}
hasMore={false}
onContractClick={(contract) => {
addContractToGroup(contract)
}}
overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'}
hideQuickBet={true}
/>
) : (
<LoadingIndicator />
)}
</div> </div>
</Col> </Col>
</Modal> </Modal>
<Row className={'items-center justify-center'}> <Row className={'items-center justify-center'}>
<button <button
className={ className={
'btn btn-sm btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case' 'btn btn-md btn-outline cursor-pointer gap-2 whitespace-nowrap text-sm normal-case'
} }
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
<PlusIcon className="mr-1 h-5 w-5" /> Add an old question
Add old questions to this group
</button> </button>
</Row> </Row>
</> </>
@ -591,17 +564,11 @@ function JoinGroupButton(props: {
const { group, user } = props const { group, user } = props
function joinGroup() { function joinGroup() {
if (user && !group.memberIds.includes(user.id)) { if (user && !group.memberIds.includes(user.id)) {
toast.promise( toast.promise(addUserToGroup(group, user.id), {
updateGroup(group, { loading: 'Joining group...',
...group, success: 'Joined group!',
memberIds: [...group.memberIds, user.id], error: "Couldn't join group",
}), })
{
loading: 'Joining group...',
success: 'Joined group!',
error: "Couldn't join group",
}
)
} }
} }
return ( return (

View File

@ -64,7 +64,7 @@ export default function LinkPage() {
<Col className="w-full px-8"> <Col className="w-full px-8">
<Title text="Manalinks" /> <Title text="Manalinks" />
<Tabs <Tabs
className={'pb-2 pt-1 '} labelClassName={'pb-2 pt-1 '}
defaultIndex={0} defaultIndex={0}
tabs={[ tabs={[
{ {

View File

@ -86,7 +86,7 @@ export default function Notifications() {
<div className={'p-2 sm:p-4'}> <div className={'p-2 sm:p-4'}>
<Title text={'Notifications'} className={'hidden md:block'} /> <Title text={'Notifications'} className={'hidden md:block'} />
<Tabs <Tabs
className={'pb-2 pt-1 '} labelClassName={'pb-2 pt-1 '}
defaultIndex={0} defaultIndex={0}
tabs={[ tabs={[
{ {