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;
|
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())
|
||||||
|
|
|
@ -5,7 +5,13 @@ import Stripe from 'stripe'
|
||||||
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
||||||
import { sendThankYouEmail } from './emails'
|
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 = {
|
export type StripeTransaction = {
|
||||||
userId: string
|
userId: string
|
||||||
|
|
|
@ -101,7 +101,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bets.length === 0) return <NoBets />
|
if (bets.length === 0) return <NoBets user={user} />
|
||||||
// Decending creation time.
|
// Decending creation time.
|
||||||
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
||||||
const contractBets = groupBy(bets, 'contractId')
|
const contractBets = groupBy(bets, 'contractId')
|
||||||
|
@ -219,7 +219,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
||||||
|
|
||||||
<Col className="mt-6 divide-y">
|
<Col className="mt-6 divide-y">
|
||||||
{displayedContracts.length === 0 ? (
|
{displayedContracts.length === 0 ? (
|
||||||
<NoBets />
|
<NoBets user={user} />
|
||||||
) : (
|
) : (
|
||||||
displayedContracts.map((contract) => (
|
displayedContracts.map((contract) => (
|
||||||
<ContractBets
|
<ContractBets
|
||||||
|
@ -236,13 +236,20 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoBets = () => {
|
const NoBets = ({ user }: { user: User }) => {
|
||||||
|
const me = useUser()
|
||||||
return (
|
return (
|
||||||
<div className="mx-4 text-gray-500">
|
<div className="mx-4 text-gray-500">
|
||||||
You have not made any bets yet.{' '}
|
{user.id === me?.id ? (
|
||||||
<SiteLink href="/home" className="underline">
|
<>
|
||||||
Find a prediction market!
|
You have not made any bets yet.{' '}
|
||||||
</SiteLink>
|
<SiteLink href="/home" className="underline">
|
||||||
|
Find a prediction market!
|
||||||
|
</SiteLink>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>{user.name} has not made any public bets yet.</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function ConfirmationButton(props: {
|
||||||
id: string
|
id: string
|
||||||
openModalBtn: {
|
openModalBtn: {
|
||||||
label: string
|
label: string
|
||||||
icon?: any
|
icon?: JSX.Element
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
cancelBtn?: {
|
cancelBtn?: {
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { Menu, Transition } from '@headlessui/react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export function MenuButton(props: {
|
export function MenuButton(props: {
|
||||||
buttonContent: any
|
buttonContent: JSX.Element
|
||||||
menuItems: { name: string; href: string; onClick?: () => void }[]
|
menuItems: { name: string; href: string; onClick?: () => void }[]
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { auth } from './users'
|
import { auth } from './users'
|
||||||
|
import { FIREBASE_CONFIG } from 'common/envs/constants'
|
||||||
|
|
||||||
export class APIError extends Error {
|
export class APIError extends Error {
|
||||||
code: number
|
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) {
|
export function createContract(params: any) {
|
||||||
return call('/api/v0/market', 'POST', params)
|
return call(getFunctionUrl('createContract'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function placeBet(params: any) {
|
export function placeBet(params: any) {
|
||||||
return call('/api/v0/bets', 'POST', params)
|
return call(getFunctionUrl('placeBet'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -110,11 +110,13 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Bid = { yesBid: number; noBid: number }
|
||||||
|
|
||||||
function NewBidTable(props: {
|
function NewBidTable(props: {
|
||||||
steps: number
|
steps: number
|
||||||
bids: Array<{ yesBid: number; noBid: number }>
|
bids: Array<Bid>
|
||||||
setSteps: (steps: number) => void
|
setSteps: (steps: number) => void
|
||||||
setBids: (bids: any[]) => void
|
setBids: (bids: Array<Bid>) => void
|
||||||
}) {
|
}) {
|
||||||
const { steps, bids, setSteps, setBids } = props
|
const { steps, bids, setSteps, setBids } = props
|
||||||
// Prepare for new bids
|
// Prepare for new bids
|
||||||
|
|
Loading…
Reference in New Issue
Block a user