Merge branch 'main' into automated-market-resolution
This commit is contained in:
commit
f7bc69d76e
|
@ -24,6 +24,11 @@ service cloud.firestore {
|
|||
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} {
|
||||
allow read: if resource.data.id == request.auth.uid || isAdmin();
|
||||
allow update: if (resource.data.id == request.auth.uid || isAdmin())
|
||||
|
|
|
@ -5,7 +5,13 @@ import Stripe from 'stripe'
|
|||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
||||
import { sendThankYouEmail } from './emails'
|
||||
|
||||
export type StripeSession = Stripe.Event.Data.Object & { id: any, metadata: any}
|
||||
export type StripeSession = Stripe.Event.Data.Object & {
|
||||
id: string
|
||||
metadata: {
|
||||
userId: string
|
||||
manticDollarQuantity: string
|
||||
}
|
||||
}
|
||||
|
||||
export type StripeTransaction = {
|
||||
userId: string
|
||||
|
|
|
@ -101,7 +101,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
|||
return <LoadingIndicator />
|
||||
}
|
||||
|
||||
if (bets.length === 0) return <NoBets />
|
||||
if (bets.length === 0) return <NoBets user={user} />
|
||||
// Decending creation time.
|
||||
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
||||
const contractBets = groupBy(bets, 'contractId')
|
||||
|
@ -219,7 +219,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
|||
|
||||
<Col className="mt-6 divide-y">
|
||||
{displayedContracts.length === 0 ? (
|
||||
<NoBets />
|
||||
<NoBets user={user} />
|
||||
) : (
|
||||
displayedContracts.map((contract) => (
|
||||
<ContractBets
|
||||
|
@ -236,13 +236,20 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
const NoBets = () => {
|
||||
const NoBets = ({ user }: { user: User }) => {
|
||||
const me = useUser()
|
||||
return (
|
||||
<div className="mx-4 text-gray-500">
|
||||
You have not made any bets yet.{' '}
|
||||
<SiteLink href="/home" className="underline">
|
||||
Find a prediction market!
|
||||
</SiteLink>
|
||||
{user.id === me?.id ? (
|
||||
<>
|
||||
You have not made any bets yet.{' '}
|
||||
<SiteLink href="/home" className="underline">
|
||||
Find a prediction market!
|
||||
</SiteLink>
|
||||
</>
|
||||
) : (
|
||||
<>{user.name} has not made any public bets yet.</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export function ConfirmationButton(props: {
|
|||
id: string
|
||||
openModalBtn: {
|
||||
label: string
|
||||
icon?: any
|
||||
icon?: JSX.Element
|
||||
className?: string
|
||||
}
|
||||
cancelBtn?: {
|
||||
|
|
|
@ -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<string>('all')
|
||||
const showFollows = category === 'following'
|
||||
const followsKey =
|
||||
showFollows && follows?.length ? `${follows.join(',')}` : ''
|
||||
|
||||
if (!sort) return <></>
|
||||
|
||||
const indexName = `${indexPrefix}contracts-${sort}`
|
||||
|
||||
return (
|
||||
<InstantSearch
|
||||
searchClient={searchClient}
|
||||
indexName={`${indexPrefix}contracts-${sort}`}
|
||||
indexName={indexName}
|
||||
key={`search-${
|
||||
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
||||
}`}
|
||||
}${followsKey}`}
|
||||
initialUiState={
|
||||
showFollows
|
||||
? {
|
||||
[indexName]: {
|
||||
refinementList: {
|
||||
creatorId: ['', ...(follows ?? [])],
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Row className="gap-1 sm:gap-2">
|
||||
<SearchBox
|
||||
|
@ -120,11 +139,20 @@ export function ContractSearch(props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
<ContractSearchInner
|
||||
querySortOptions={querySortOptions}
|
||||
filter={filter}
|
||||
additionalFilter={{ category, ...additionalFilter }}
|
||||
/>
|
||||
<Spacer h={4} />
|
||||
|
||||
{showFollows && (follows ?? []).length === 0 ? (
|
||||
<>You're not following anyone yet.</>
|
||||
) : (
|
||||
<ContractSearchInner
|
||||
querySortOptions={querySortOptions}
|
||||
filter={filter}
|
||||
additionalFilter={{
|
||||
category: category === 'following' ? 'all' : category,
|
||||
...additionalFilter,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</InstantSearch>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,15 @@ export function CategorySelector(props: {
|
|||
}}
|
||||
/>
|
||||
|
||||
<CategoryButton
|
||||
key="following"
|
||||
category="Following"
|
||||
isFollowed={category === 'following'}
|
||||
toggle={() => {
|
||||
setCategory('following')
|
||||
}}
|
||||
/>
|
||||
|
||||
{CATEGORY_LIST.map((cat) => (
|
||||
<CategoryButton
|
||||
key={cat}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx'
|
||||
import { Fold } from 'common/fold'
|
||||
import { useFollowedFoldIds } from 'web/hooks/use-fold'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { followFold, unfollowFold } from 'web/lib/firebase/folds'
|
||||
import { FollowButton } from '../follow-button'
|
||||
|
||||
export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||
const { fold, className } = props
|
||||
|
@ -10,7 +10,7 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
|||
const user = useUser()
|
||||
|
||||
const followedFoldIds = useFollowedFoldIds(user)
|
||||
const following = followedFoldIds
|
||||
const isFollowing = followedFoldIds
|
||||
? followedFoldIds.includes(fold.id)
|
||||
: undefined
|
||||
|
||||
|
@ -22,27 +22,12 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
|||
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 (
|
||||
<button className={clsx('btn btn-sm', className)} onClick={onFollow}>
|
||||
Follow
|
||||
</button>
|
||||
<FollowButton
|
||||
isFollowing={isFollowing}
|
||||
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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}) {
|
||||
|
|
|
@ -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 (
|
||||
<Page>
|
||||
<SEO
|
||||
|
@ -116,6 +128,13 @@ export function UserPage(props: {
|
|||
|
||||
{/* Top right buttons (e.g. edit, follow) */}
|
||||
<div className="absolute right-0 top-0 mt-4 mr-4">
|
||||
{!isCurrentUser && (
|
||||
<FollowButton
|
||||
isFollowing={isFollowing}
|
||||
onFollow={onFollow}
|
||||
onUnfollow={onUnfollow}
|
||||
/>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
<SiteLink className="btn" href="/profile">
|
||||
<PencilIcon className="h-5 w-5" />{' '}
|
||||
|
@ -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
|
||||
|
|
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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<Bid>
|
||||
setSteps: (steps: number) => void
|
||||
setBids: (bids: any[]) => void
|
||||
setBids: (bids: Array<Bid>) => void
|
||||
}) {
|
||||
const { steps, bids, setSteps, setBids } = props
|
||||
// Prepare for new bids
|
||||
|
|
Loading…
Reference in New Issue
Block a user