Follow other users. Filter markets by followed (#387)
* Add follow button to user page * Update follows in the database using follow button. * Add toggle for followed market creators to home * Hide follow toggle from user's markets page * Check that sold bet is by auth'd user * Change follow toggle to category pill * Remove unused imports * Remove console.logs
This commit is contained in:
parent
749f7aad40
commit
c1bda8a775
|
@ -24,6 +24,11 @@ service cloud.firestore {
|
||||||
allow write: if request.auth.uid == userId;
|
allow write: if request.auth.uid == userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /users/{userId}/follows/{followUserId} {
|
||||||
|
allow read;
|
||||||
|
allow write: if request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
match /private-users/{userId} {
|
match /private-users/{userId} {
|
||||||
allow read: if resource.data.id == request.auth.uid || isAdmin();
|
allow read: if resource.data.id == request.auth.uid || isAdmin();
|
||||||
allow update: if (resource.data.id == request.auth.uid || isAdmin())
|
allow update: if (resource.data.id == request.auth.uid || isAdmin())
|
||||||
|
|
|
@ -20,10 +20,10 @@ import { ContractsGrid } from './contract/contracts-list'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { ENV } from 'common/envs/constants'
|
import { ENV } from 'common/envs/constants'
|
||||||
import { CategorySelector } from './feed/category-selector'
|
import { CategorySelector } from './feed/category-selector'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
'GJQPAYENIF',
|
'GJQPAYENIF',
|
||||||
|
@ -60,6 +60,8 @@ export function ContractSearch(props: {
|
||||||
const { querySortOptions, additionalFilter, showCategorySelector } = props
|
const { querySortOptions, additionalFilter, showCategorySelector } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const follows = useFollows(user?.id)
|
||||||
|
|
||||||
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
||||||
|
|
||||||
const sort = sortIndexes
|
const sort = sortIndexes
|
||||||
|
@ -73,15 +75,32 @@ export function ContractSearch(props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
const [category, setCategory] = useState<string>('all')
|
const [category, setCategory] = useState<string>('all')
|
||||||
|
const showFollows = category === 'following'
|
||||||
|
const followsKey =
|
||||||
|
showFollows && follows?.length ? `${follows.join(',')}` : ''
|
||||||
|
|
||||||
if (!sort) return <></>
|
if (!sort) return <></>
|
||||||
|
|
||||||
|
const indexName = `${indexPrefix}contracts-${sort}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstantSearch
|
<InstantSearch
|
||||||
searchClient={searchClient}
|
searchClient={searchClient}
|
||||||
indexName={`${indexPrefix}contracts-${sort}`}
|
indexName={indexName}
|
||||||
key={`search-${
|
key={`search-${
|
||||||
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
||||||
}`}
|
}${followsKey}`}
|
||||||
|
initialUiState={
|
||||||
|
showFollows
|
||||||
|
? {
|
||||||
|
[indexName]: {
|
||||||
|
refinementList: {
|
||||||
|
creatorId: ['', ...(follows ?? [])],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Row className="gap-1 sm:gap-2">
|
<Row className="gap-1 sm:gap-2">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
|
@ -120,11 +139,20 @@ export function ContractSearch(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContractSearchInner
|
<Spacer h={4} />
|
||||||
querySortOptions={querySortOptions}
|
|
||||||
filter={filter}
|
{showFollows && (follows ?? []).length === 0 ? (
|
||||||
additionalFilter={{ category, ...additionalFilter }}
|
<>You're not following anyone yet.</>
|
||||||
/>
|
) : (
|
||||||
|
<ContractSearchInner
|
||||||
|
querySortOptions={querySortOptions}
|
||||||
|
filter={filter}
|
||||||
|
additionalFilter={{
|
||||||
|
category: category === 'following' ? 'all' : category,
|
||||||
|
...additionalFilter,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</InstantSearch>
|
</InstantSearch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,15 @@ export function CategorySelector(props: {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CategoryButton
|
||||||
|
key="following"
|
||||||
|
category="Following"
|
||||||
|
isFollowed={category === 'following'}
|
||||||
|
toggle={() => {
|
||||||
|
setCategory('following')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{CATEGORY_LIST.map((cat) => (
|
{CATEGORY_LIST.map((cat) => (
|
||||||
<CategoryButton
|
<CategoryButton
|
||||||
key={cat}
|
key={cat}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Fold } from 'common/fold'
|
import { Fold } from 'common/fold'
|
||||||
import { useFollowedFoldIds } from 'web/hooks/use-fold'
|
import { useFollowedFoldIds } from 'web/hooks/use-fold'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { followFold, unfollowFold } from 'web/lib/firebase/folds'
|
import { followFold, unfollowFold } from 'web/lib/firebase/folds'
|
||||||
|
import { FollowButton } from '../follow-button'
|
||||||
|
|
||||||
export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
const { fold, className } = props
|
const { fold, className } = props
|
||||||
|
@ -10,7 +10,7 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const followedFoldIds = useFollowedFoldIds(user)
|
const followedFoldIds = useFollowedFoldIds(user)
|
||||||
const following = followedFoldIds
|
const isFollowing = followedFoldIds
|
||||||
? followedFoldIds.includes(fold.id)
|
? followedFoldIds.includes(fold.id)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
@ -22,27 +22,12 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
if (user) unfollowFold(fold, user)
|
if (user) unfollowFold(fold, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user || following === undefined)
|
|
||||||
return (
|
|
||||||
<button className={clsx('btn btn-sm invisible', className)}>
|
|
||||||
Follow
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (following) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={clsx('btn btn-outline btn-sm', className)}
|
|
||||||
onClick={onUnfollow}
|
|
||||||
>
|
|
||||||
Following
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={clsx('btn btn-sm', className)} onClick={onFollow}>
|
<FollowButton
|
||||||
Follow
|
isFollowing={isFollowing}
|
||||||
</button>
|
onFollow={onFollow}
|
||||||
|
onUnfollow={onUnfollow}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
37
web/components/follow-button.tsx
Normal file
37
web/components/follow-button.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
|
||||||
|
export function FollowButton(props: {
|
||||||
|
isFollowing: boolean | undefined
|
||||||
|
onFollow: () => void
|
||||||
|
onUnfollow: () => void
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { isFollowing, onFollow, onUnfollow, className } = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
if (!user || isFollowing === undefined)
|
||||||
|
return (
|
||||||
|
<button className={clsx('btn btn-sm invisible', className)}>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isFollowing) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx('btn btn-outline btn-sm', className)}
|
||||||
|
onClick={onUnfollow}
|
||||||
|
>
|
||||||
|
Following
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={clsx('btn btn-sm', className)} onClick={onFollow}>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { follow, unfollow, User } from 'web/lib/firebase/users'
|
||||||
import { CreatorContractsList } from './contract/contracts-list'
|
import { CreatorContractsList } from './contract/contracts-list'
|
||||||
import { SEO } from './SEO'
|
import { SEO } from './SEO'
|
||||||
import { Page } from './page'
|
import { Page } from './page'
|
||||||
|
@ -89,6 +89,18 @@ export function UserPage(props: {
|
||||||
})
|
})
|
||||||
}, [usersComments])
|
}, [usersComments])
|
||||||
|
|
||||||
|
const yourFollows = useFollows(currentUser?.id)
|
||||||
|
const isFollowing = yourFollows?.includes(user.id)
|
||||||
|
|
||||||
|
const onFollow = () => {
|
||||||
|
if (!currentUser) return
|
||||||
|
follow(currentUser.id, user.id)
|
||||||
|
}
|
||||||
|
const onUnfollow = () => {
|
||||||
|
if (!currentUser) return
|
||||||
|
unfollow(currentUser.id, user.id)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -116,6 +128,13 @@ export function UserPage(props: {
|
||||||
|
|
||||||
{/* Top right buttons (e.g. edit, follow) */}
|
{/* Top right buttons (e.g. edit, follow) */}
|
||||||
<div className="absolute right-0 top-0 mt-4 mr-4">
|
<div className="absolute right-0 top-0 mt-4 mr-4">
|
||||||
|
{!isCurrentUser && (
|
||||||
|
<FollowButton
|
||||||
|
isFollowing={isFollowing}
|
||||||
|
onFollow={onFollow}
|
||||||
|
onUnfollow={onUnfollow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<SiteLink className="btn" href="/profile">
|
<SiteLink className="btn" href="/profile">
|
||||||
<PencilIcon className="h-5 w-5" />{' '}
|
<PencilIcon className="h-5 w-5" />{' '}
|
||||||
|
@ -281,6 +300,8 @@ export function defaultBannerUrl(userId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
import { ExclamationIcon } from '@heroicons/react/solid'
|
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||||
|
import { FollowButton } from './follow-button'
|
||||||
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
|
|
||||||
function AlertBox(props: { title: string; text: string }) {
|
function AlertBox(props: { title: string; text: string }) {
|
||||||
const { title, text } = props
|
const { title, text } = props
|
||||||
|
|
12
web/hooks/use-follows.ts
Normal file
12
web/hooks/use-follows.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { listenForFollows } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export const useFollows = (userId: string | undefined) => {
|
||||||
|
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) return listenForFollows(userId, setFollowIds)
|
||||||
|
}, [userId])
|
||||||
|
|
||||||
|
return followIds
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getDocs,
|
getDocs,
|
||||||
orderBy,
|
orderBy,
|
||||||
updateDoc,
|
updateDoc,
|
||||||
|
deleteDoc,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { getAuth } from 'firebase/auth'
|
import { getAuth } from 'firebase/auth'
|
||||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||||
|
@ -239,3 +240,26 @@ export async function getCategoryFeeds(userId: string) {
|
||||||
const feeds = feedData.map((data) => data?.feed ?? [])
|
const feeds = feedData.map((data) => data?.feed ?? [])
|
||||||
return Object.fromEntries(zip(CATEGORY_LIST, feeds) as [string, feed][])
|
return Object.fromEntries(zip(CATEGORY_LIST, feeds) as [string, feed][])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function follow(userId: string, followedUserId: string) {
|
||||||
|
const followDoc = doc(db, 'users', userId, 'follows', followedUserId)
|
||||||
|
await setDoc(followDoc, {
|
||||||
|
userId: followedUserId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unfollow(userId: string, unfollowedUserId: string) {
|
||||||
|
const followDoc = doc(db, 'users', userId, 'follows', unfollowedUserId)
|
||||||
|
await deleteDoc(followDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenForFollows(
|
||||||
|
userId: string,
|
||||||
|
setFollowIds: (followIds: string[]) => void
|
||||||
|
) {
|
||||||
|
const follows = collection(db, 'users', userId, 'follows')
|
||||||
|
return listenForValues<{ userId: string }>(follows, (docs) =>
|
||||||
|
setFollowIds(docs.map(({ userId }) => userId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user