- You have not made any bets yet.{' '}
-
- Find a prediction market!
-
+ {user.id === me?.id ? (
+ <>
+ You have not made any bets yet.{' '}
+
+ Find a prediction market!
+
+ >
+ ) : (
+ <>{user.name} has not made any public bets yet.>
+ )}
)
}
diff --git a/web/components/confirmation-button.tsx b/web/components/confirmation-button.tsx
index e07b6dab..e895467a 100644
--- a/web/components/confirmation-button.tsx
+++ b/web/components/confirmation-button.tsx
@@ -8,7 +8,7 @@ export function ConfirmationButton(props: {
id: string
openModalBtn: {
label: string
- icon?: any
+ icon?: JSX.Element
className?: string
}
cancelBtn?: {
diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx
index 65a70d8e..6371064e 100644
--- a/web/components/contract-search.tsx
+++ b/web/components/contract-search.tsx
@@ -20,10 +20,10 @@ import { ContractsGrid } from './contract/contracts-list'
import { Row } from './layout/row'
import { useEffect, useRef, useState } from 'react'
import { Spacer } from './layout/spacer'
-import { useRouter } from 'next/router'
import { ENV } from 'common/envs/constants'
import { CategorySelector } from './feed/category-selector'
import { useUser } from 'web/hooks/use-user'
+import { useFollows } from 'web/hooks/use-follows'
const searchClient = algoliasearch(
'GJQPAYENIF',
@@ -60,6 +60,8 @@ export function ContractSearch(props: {
const { querySortOptions, additionalFilter, showCategorySelector } = props
const user = useUser()
+ const follows = useFollows(user?.id)
+
const { initialSort } = useInitialQueryAndSort(querySortOptions)
const sort = sortIndexes
@@ -73,15 +75,32 @@ export function ContractSearch(props: {
)
const [category, setCategory] = useState('all')
+ const showFollows = category === 'following'
+ const followsKey =
+ showFollows && follows?.length ? `${follows.join(',')}` : ''
if (!sort) return <>>
+
+ const indexName = `${indexPrefix}contracts-${sort}`
+
return (
)}
-
+
+
+ {showFollows && (follows ?? []).length === 0 ? (
+ <>You're not following anyone yet.>
+ ) : (
+
+ )}
)
}
diff --git a/web/components/feed/category-selector.tsx b/web/components/feed/category-selector.tsx
index 7c014a0e..9ae5fd93 100644
--- a/web/components/feed/category-selector.tsx
+++ b/web/components/feed/category-selector.tsx
@@ -27,6 +27,15 @@ export function CategorySelector(props: {
}}
/>
+ {
+ setCategory('following')
+ }}
+ />
+
{CATEGORY_LIST.map((cat) => (
- Follow
-
- )
-
- if (following) {
- return (
-
- )
- }
-
return (
-
+
)
}
diff --git a/web/components/follow-button.tsx b/web/components/follow-button.tsx
new file mode 100644
index 00000000..f7acf7a9
--- /dev/null
+++ b/web/components/follow-button.tsx
@@ -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 (
+
+ )
+
+ if (isFollowing) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/web/components/nav/menu.tsx b/web/components/nav/menu.tsx
index 9d348fab..24fb35a4 100644
--- a/web/components/nav/menu.tsx
+++ b/web/components/nav/menu.tsx
@@ -3,7 +3,7 @@ import { Menu, Transition } from '@headlessui/react'
import clsx from 'clsx'
export function MenuButton(props: {
- buttonContent: any
+ buttonContent: JSX.Element
menuItems: { name: string; href: string; onClick?: () => void }[]
className?: string
}) {
diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx
index c1099ce8..0c704056 100644
--- a/web/components/user-page.tsx
+++ b/web/components/user-page.tsx
@@ -1,5 +1,5 @@
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 { SEO } from './SEO'
import { Page } from './page'
@@ -89,6 +89,18 @@ export function UserPage(props: {
})
}, [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 (
+ {!isCurrentUser && (
+
+ )}
{isCurrentUser && (
{' '}
@@ -281,6 +300,8 @@ export function defaultBannerUrl(userId: string) {
}
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 }) {
const { title, text } = props
diff --git a/web/hooks/use-follows.ts b/web/hooks/use-follows.ts
new file mode 100644
index 00000000..a8a775d8
--- /dev/null
+++ b/web/hooks/use-follows.ts
@@ -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()
+
+ useEffect(() => {
+ if (userId) return listenForFollows(userId, setFollowIds)
+ }, [userId])
+
+ return followIds
+}
diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts
index 3fa93004..08f26ff4 100644
--- a/web/lib/firebase/api-call.ts
+++ b/web/lib/firebase/api-call.ts
@@ -1,4 +1,5 @@
import { auth } from './users'
+import { FIREBASE_CONFIG } from 'common/envs/constants'
export class APIError extends Error {
code: number
@@ -32,10 +33,20 @@ export async function call(url: string, method: string, params: any) {
})
}
+// Our users access the API through the Vercel proxy routes at /api/v0/blah,
+// but right now at least until we get performance under control let's have the
+// app just hit the cloud functions directly -- there's no difference and it's
+// one less hop
+
+function getFunctionUrl(name: string) {
+ const { projectId, region } = FIREBASE_CONFIG
+ return `https://${region}-${projectId}.cloudfunctions.net/${name}`
+}
+
export function createContract(params: any) {
- return call('/api/v0/market', 'POST', params)
+ return call(getFunctionUrl('createContract'), 'POST', params)
}
export function placeBet(params: any) {
- return call('/api/v0/bets', 'POST', params)
+ return call(getFunctionUrl('placeBet'), 'POST', params)
}
diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts
index 1e316744..61b4fbb3 100644
--- a/web/lib/firebase/users.ts
+++ b/web/lib/firebase/users.ts
@@ -10,6 +10,7 @@ import {
getDocs,
orderBy,
updateDoc,
+ deleteDoc,
} from 'firebase/firestore'
import { getAuth } from 'firebase/auth'
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 ?? [])
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))
+ )
+}
diff --git a/web/pages/simulator.tsx b/web/pages/simulator.tsx
index dcf44478..dc6ca873 100644
--- a/web/pages/simulator.tsx
+++ b/web/pages/simulator.tsx
@@ -110,11 +110,13 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
}
}
+type Bid = { yesBid: number; noBid: number }
+
function NewBidTable(props: {
steps: number
- bids: Array<{ yesBid: number; noBid: number }>
+ bids: Array
setSteps: (steps: number) => void
- setBids: (bids: any[]) => void
+ setBids: (bids: Array) => void
}) {
const { steps, bids, setSteps, setBids } = props
// Prepare for new bids