Categories (#132)
* basic market categories * use tags to store market category * display category in market * display full category * category selector component on feed * Move feed data fetching to new file * Decrease batch size for updating feed to prevent out-of-memory error * Compute and update category feeds! * Show feeds based on category tabs * Add react-query package! * Use react query to cache contracts * Remove 'other' category * Add back personal / friends to feed categories * Show scrollbar temporarily for categories * Remove 5 categories, change geopolitics to world * finance => economics * Show categories on two lines on larger screens Co-authored-by: James Grugett <jahooma@gmail.com>
This commit is contained in:
parent
403156ed1a
commit
9a4e5763f5
25
common/categories.ts
Normal file
25
common/categories.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export const CATEGORIES = {
|
||||||
|
politics: 'Politics',
|
||||||
|
technology: 'Technology',
|
||||||
|
sports: 'Sports',
|
||||||
|
gaming: 'Gaming / Esports',
|
||||||
|
manifold: 'Manifold Markets',
|
||||||
|
science: 'Science',
|
||||||
|
world: 'World',
|
||||||
|
fun: 'Fun stuff',
|
||||||
|
personal: 'Personal',
|
||||||
|
economics: 'Economics',
|
||||||
|
crypto: 'Crypto',
|
||||||
|
health: 'Health',
|
||||||
|
// entertainment: 'Entertainment',
|
||||||
|
// society: 'Society',
|
||||||
|
// friends: 'Friends / Community',
|
||||||
|
// business: 'Business',
|
||||||
|
// charity: 'Charities / Non-profits',
|
||||||
|
} as { [category: string]: string }
|
||||||
|
|
||||||
|
export const TO_CATEGORY = Object.fromEntries(
|
||||||
|
Object.entries(CATEGORIES).map(([k, v]) => [v, k])
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CATEGORY_LIST = Object.keys(CATEGORIES)
|
9
common/feed.ts
Normal file
9
common/feed.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Bet } from './bet'
|
||||||
|
import { Comment } from './comment'
|
||||||
|
import { Contract } from './contract'
|
||||||
|
|
||||||
|
export type feed = {
|
||||||
|
contract: Contract
|
||||||
|
recentBets: Bet[]
|
||||||
|
recentComments: Comment[]
|
||||||
|
}[]
|
|
@ -17,6 +17,8 @@ export type User = {
|
||||||
totalDeposits: number
|
totalDeposits: number
|
||||||
totalPnLCached: number
|
totalPnLCached: number
|
||||||
creatorVolumeCached: number
|
creatorVolumeCached: number
|
||||||
|
|
||||||
|
followedCategories?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STARTING_BALANCE = 1000
|
export const STARTING_BALANCE = 1000
|
||||||
|
|
|
@ -16,7 +16,7 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
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']);
|
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId} {
|
match /private-users/{userId} {
|
||||||
|
@ -35,7 +35,7 @@ service cloud.firestore {
|
||||||
allow create: if userId == request.auth.uid;
|
allow create: if userId == request.auth.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId}/cache/feed {
|
match /private-users/{userId}/cache/{docId} {
|
||||||
allow read: if userId == request.auth.uid || isAdmin();
|
allow read: if userId == request.auth.uid || isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
},
|
},
|
||||||
"main": "lib/functions/src/index.js",
|
"main": "lib/functions/src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-query-firebase/firestore": "0.4.2",
|
||||||
"fetch": "1.1.0",
|
"fetch": "1.1.0",
|
||||||
"firebase-admin": "10.0.0",
|
"firebase-admin": "10.0.0",
|
||||||
"firebase-functions": "3.16.0",
|
"firebase-functions": "3.16.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mailgun-js": "0.22.0",
|
"mailgun-js": "0.22.0",
|
||||||
|
"react-query": "3.39.0",
|
||||||
"module-alias": "2.2.2",
|
"module-alias": "2.2.2",
|
||||||
"stripe": "8.194.0"
|
"stripe": "8.194.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { getNewMultiBetInfo } from 'common/new-bet'
|
||||||
import { Answer, MAX_ANSWER_LENGTH } from 'common/answer'
|
import { Answer, MAX_ANSWER_LENGTH } from 'common/answer'
|
||||||
import { getContract, getValues } from './utils'
|
import { getContract, getValues } from './utils'
|
||||||
import { sendNewAnswerEmail } from './emails'
|
import { sendNewAnswerEmail } from './emails'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { hasUserHitManaLimit } from 'common/calculate'
|
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||||
|
|
||||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
async (
|
async (
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { chargeUser, getUser } from './utils'
|
import { chargeUser, getUser } from './utils'
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
|
71
functions/src/get-feed-data.ts
Normal file
71
functions/src/get-feed-data.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
import { getValues } from './utils'
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export async function getFeedContracts() {
|
||||||
|
// Get contracts bet on or created in last week.
|
||||||
|
const [activeContracts, inactiveContracts] = await Promise.all([
|
||||||
|
getValues<Contract>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('isResolved', '==', false)
|
||||||
|
.where('volume7Days', '>', 0)
|
||||||
|
),
|
||||||
|
|
||||||
|
getValues<Contract>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('isResolved', '==', false)
|
||||||
|
.where('createdTime', '>', Date.now() - DAY_MS * 7)
|
||||||
|
.where('volume7Days', '==', 0)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
const combined = [...activeContracts, ...inactiveContracts]
|
||||||
|
// Remove closed contracts.
|
||||||
|
return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTaggedContracts(tag: string) {
|
||||||
|
const taggedContracts = await getValues<Contract>(
|
||||||
|
firestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('isResolved', '==', false)
|
||||||
|
.where('lowercaseTags', 'array-contains', tag.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove closed contracts.
|
||||||
|
return taggedContracts.filter((c) => (c.closeTime ?? Infinity) > Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecentBetsAndComments(contract: Contract) {
|
||||||
|
const contractDoc = firestore.collection('contracts').doc(contract.id)
|
||||||
|
|
||||||
|
const [recentBets, recentComments] = await Promise.all([
|
||||||
|
getValues<Bet>(
|
||||||
|
contractDoc
|
||||||
|
.collection('bets')
|
||||||
|
.where('createdTime', '>', Date.now() - DAY_MS)
|
||||||
|
.orderBy('createdTime', 'desc')
|
||||||
|
.limit(1)
|
||||||
|
),
|
||||||
|
|
||||||
|
getValues<Comment>(
|
||||||
|
contractDoc
|
||||||
|
.collection('comments')
|
||||||
|
.where('createdTime', '>', Date.now() - 3 * DAY_MS)
|
||||||
|
.orderBy('createdTime', 'desc')
|
||||||
|
.limit(3)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
contract,
|
||||||
|
recentBets,
|
||||||
|
recentComments,
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,9 @@ import { User } from 'common/user'
|
||||||
import { batchedWaitAll } from 'common/util/promise'
|
import { batchedWaitAll } from 'common/util/promise'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { updateWordScores } from '../update-recommendations'
|
import { updateWordScores } from '../update-recommendations'
|
||||||
import { getFeedContracts, doUserFeedUpdate } from '../update-feed'
|
import { computeFeed } from '../update-feed'
|
||||||
|
import { getFeedContracts, getTaggedContracts } from '../get-feed-data'
|
||||||
|
import { CATEGORY_LIST } from '../../../common/categories'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -19,8 +21,7 @@ async function updateFeed() {
|
||||||
const contracts = await getValues<Contract>(firestore.collection('contracts'))
|
const contracts = await getValues<Contract>(firestore.collection('contracts'))
|
||||||
const feedContracts = await getFeedContracts()
|
const feedContracts = await getFeedContracts()
|
||||||
const users = await getValues<User>(
|
const users = await getValues<User>(
|
||||||
firestore.collection('users')
|
firestore.collection('users').where('username', '==', 'JamesGrugett')
|
||||||
// .where('username', '==', 'JamesGrugett')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await batchedWaitAll(
|
await batchedWaitAll(
|
||||||
|
@ -28,7 +29,22 @@ async function updateFeed() {
|
||||||
console.log('Updating recs for', user.username)
|
console.log('Updating recs for', user.username)
|
||||||
await updateWordScores(user, contracts)
|
await updateWordScores(user, contracts)
|
||||||
console.log('Updating feed for', user.username)
|
console.log('Updating feed for', user.username)
|
||||||
await doUserFeedUpdate(user, feedContracts)
|
await computeFeed(user, feedContracts)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Updating feed categories!')
|
||||||
|
|
||||||
|
await batchedWaitAll(
|
||||||
|
users.map((user) => async () => {
|
||||||
|
for (const category of CATEGORY_LIST) {
|
||||||
|
const contracts = await getTaggedContracts(category)
|
||||||
|
const feed = await computeFeed(user, contracts)
|
||||||
|
await firestore
|
||||||
|
.collection(`private-users/${user.id}/cache`)
|
||||||
|
.doc(`feed-${category}`)
|
||||||
|
.set({ feed })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,19 @@ import {
|
||||||
getProbability,
|
getProbability,
|
||||||
getOutcomeProbability,
|
getOutcomeProbability,
|
||||||
getTopAnswer,
|
getTopAnswer,
|
||||||
} from 'common/calculate'
|
} from '../../common/calculate'
|
||||||
import { Bet } from 'common/bet'
|
import { User } from '../../common/user'
|
||||||
import { Comment } from 'common/comment'
|
|
||||||
import { User } from 'common/user'
|
|
||||||
import {
|
import {
|
||||||
getContractScore,
|
getContractScore,
|
||||||
MAX_FEED_CONTRACTS,
|
MAX_FEED_CONTRACTS,
|
||||||
} from 'common/recommended-contracts'
|
} from 'common/recommended-contracts'
|
||||||
import { callCloudFunction } from './call-cloud-function'
|
import { callCloudFunction } from './call-cloud-function'
|
||||||
|
import {
|
||||||
|
getFeedContracts,
|
||||||
|
getRecentBetsAndComments,
|
||||||
|
getTaggedContracts,
|
||||||
|
} from './get-feed-data'
|
||||||
|
import { CATEGORY_LIST } from '../../common/categories'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -28,16 +32,28 @@ export const updateFeed = functions.pubsub
|
||||||
const users = await getValues<User>(firestore.collection('users'))
|
const users = await getValues<User>(firestore.collection('users'))
|
||||||
|
|
||||||
const batchSize = 100
|
const batchSize = 100
|
||||||
const userBatches: User[][] = []
|
let userBatches: User[][] = []
|
||||||
for (let i = 0; i < users.length; i += batchSize) {
|
for (let i = 0; i < users.length; i += batchSize) {
|
||||||
userBatches.push(users.slice(i, i + batchSize))
|
userBatches.push(users.slice(i, i + batchSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('updating feed batch')
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
userBatches.map(async (users) =>
|
userBatches.map((users) =>
|
||||||
callCloudFunction('updateFeedBatch', { users })
|
callCloudFunction('updateFeedBatch', { users })
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log('updating category feed')
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
CATEGORY_LIST.map((category) =>
|
||||||
|
callCloudFunction('updateCategoryFeed', {
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateFeedBatch = functions.https.onCall(
|
export const updateFeedBatch = functions.https.onCall(
|
||||||
|
@ -45,40 +61,56 @@ export const updateFeedBatch = functions.https.onCall(
|
||||||
const { users } = data
|
const { users } = data
|
||||||
const contracts = await getFeedContracts()
|
const contracts = await getFeedContracts()
|
||||||
|
|
||||||
await Promise.all(users.map((user) => doUserFeedUpdate(user, contracts)))
|
await Promise.all(
|
||||||
}
|
users.map(async (user) => {
|
||||||
)
|
const feed = await computeFeed(user, contracts)
|
||||||
|
await getUserCacheCollection(user).doc('feed').set({ feed })
|
||||||
export async function getFeedContracts() {
|
|
||||||
// Get contracts bet on or created in last week.
|
|
||||||
const contracts = await Promise.all([
|
|
||||||
getValues<Contract>(
|
|
||||||
firestore
|
|
||||||
.collection('contracts')
|
|
||||||
.where('isResolved', '==', false)
|
|
||||||
.where('volume7Days', '>', 0)
|
|
||||||
),
|
|
||||||
|
|
||||||
getValues<Contract>(
|
|
||||||
firestore
|
|
||||||
.collection('contracts')
|
|
||||||
.where('isResolved', '==', false)
|
|
||||||
.where('createdTime', '>', Date.now() - DAY_MS * 7)
|
|
||||||
.where('volume7Days', '==', 0)
|
|
||||||
),
|
|
||||||
]).then(([activeContracts, inactiveContracts]) => {
|
|
||||||
const combined = [...activeContracts, ...inactiveContracts]
|
|
||||||
// Remove closed contracts.
|
|
||||||
return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export const updateCategoryFeed = functions.https.onCall(
|
||||||
|
async (data: { category: string }) => {
|
||||||
|
const { category } = data
|
||||||
|
const users = await getValues<User>(firestore.collection('users'))
|
||||||
|
|
||||||
return contracts
|
const batchSize = 100
|
||||||
|
const userBatches: User[][] = []
|
||||||
|
for (let i = 0; i < users.length; i += batchSize) {
|
||||||
|
userBatches.push(users.slice(i, i + batchSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const doUserFeedUpdate = async (user: User, contracts: Contract[]) => {
|
await Promise.all(
|
||||||
const userCacheCollection = firestore.collection(
|
userBatches.map(async (users) => {
|
||||||
`private-users/${user.id}/cache`
|
await callCloudFunction('updateCategoryFeedBatch', {
|
||||||
|
users,
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const updateCategoryFeedBatch = functions.https.onCall(
|
||||||
|
async (data: { users: User[]; category: string }) => {
|
||||||
|
const { users, category } = data
|
||||||
|
const contracts = await getTaggedContracts(category)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
users.map(async (user) => {
|
||||||
|
const feed = await computeFeed(user, contracts)
|
||||||
|
await getUserCacheCollection(user).doc(`feed-${category}`).set({ feed })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getUserCacheCollection = (user: User) =>
|
||||||
|
firestore.collection(`private-users/${user.id}/cache`)
|
||||||
|
|
||||||
|
export const computeFeed = async (user: User, contracts: Contract[]) => {
|
||||||
|
const userCacheCollection = getUserCacheCollection(user)
|
||||||
|
|
||||||
const [wordScores, lastViewedTime] = await Promise.all([
|
const [wordScores, lastViewedTime] = await Promise.all([
|
||||||
getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')),
|
getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')),
|
||||||
getValue<{ [contractId: string]: number }>(
|
getValue<{ [contractId: string]: number }>(
|
||||||
|
@ -109,8 +141,7 @@ export const doUserFeedUpdate = async (user: User, contracts: Contract[]) => {
|
||||||
const feed = await Promise.all(
|
const feed = await Promise.all(
|
||||||
feedContracts.map((contract) => getRecentBetsAndComments(contract))
|
feedContracts.map((contract) => getRecentBetsAndComments(contract))
|
||||||
)
|
)
|
||||||
|
return feed
|
||||||
await userCacheCollection.doc('feed').set({ feed })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreContract(
|
function scoreContract(
|
||||||
|
@ -180,31 +211,3 @@ function getLastViewedScore(viewTime: number | undefined) {
|
||||||
const frac = logInterpolation(0.5, 14, daysAgo)
|
const frac = logInterpolation(0.5, 14, daysAgo)
|
||||||
return 0.75 + 0.25 * frac
|
return 0.75 + 0.25 * frac
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRecentBetsAndComments(contract: Contract) {
|
|
||||||
const contractDoc = firestore.collection('contracts').doc(contract.id)
|
|
||||||
|
|
||||||
const [recentBets, recentComments] = await Promise.all([
|
|
||||||
getValues<Bet>(
|
|
||||||
contractDoc
|
|
||||||
.collection('bets')
|
|
||||||
.where('createdTime', '>', Date.now() - DAY_MS)
|
|
||||||
.orderBy('createdTime', 'desc')
|
|
||||||
.limit(1)
|
|
||||||
),
|
|
||||||
|
|
||||||
getValues<Comment>(
|
|
||||||
contractDoc
|
|
||||||
.collection('comments')
|
|
||||||
.where('createdTime', '>', Date.now() - 3 * DAY_MS)
|
|
||||||
.orderBy('createdTime', 'desc')
|
|
||||||
.limit(3)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
contract,
|
|
||||||
recentBets,
|
|
||||||
recentComments,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { CATEGORY_LIST } from '../../../common/categories'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { parseTags } from 'common/util/parse'
|
import { parseTags } from 'common/util/parse'
|
||||||
|
@ -9,6 +10,7 @@ import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { updateContract } from 'web/lib/firebase/contracts'
|
import { updateContract } from 'web/lib/firebase/contracts'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Linkify } from '../linkify'
|
import { Linkify } from '../linkify'
|
||||||
|
import { TagsList } from '../tags-list'
|
||||||
|
|
||||||
export function ContractDescription(props: {
|
export function ContractDescription(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -26,6 +28,7 @@ export function ContractDescription(props: {
|
||||||
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
|
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
|
||||||
)
|
)
|
||||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||||
|
|
||||||
await updateContract(contract.id, {
|
await updateContract(contract.id, {
|
||||||
description: newDescription,
|
description: newDescription,
|
||||||
tags,
|
tags,
|
||||||
|
@ -35,6 +38,9 @@ export function ContractDescription(props: {
|
||||||
|
|
||||||
if (!isCreator && !contract.description.trim()) return null
|
if (!isCreator && !contract.description.trim()) return null
|
||||||
|
|
||||||
|
const { tags } = contract
|
||||||
|
const category = tags.find((tag) => CATEGORY_LIST.includes(tag.toLowerCase()))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -43,7 +49,15 @@ export function ContractDescription(props: {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Linkify text={contract.description} />
|
<Linkify text={contract.description} />
|
||||||
|
|
||||||
|
{category && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<TagsList tags={[category]} label="Category" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
{isCreator && (
|
{isCreator && (
|
||||||
<EditContract
|
<EditContract
|
||||||
// Note: Because descriptionTimestamp is called once, later edits use
|
// Note: Because descriptionTimestamp is called once, later edits use
|
||||||
|
|
75
web/components/feed/category-selector.tsx
Normal file
75
web/components/feed/category-selector.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import { User } from '../../../common/user'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
import { CATEGORIES, CATEGORY_LIST } from '../../../common/categories'
|
||||||
|
import { updateUser } from '../../lib/firebase/users'
|
||||||
|
|
||||||
|
export function CategorySelector(props: {
|
||||||
|
user: User | null | undefined
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { className, user } = props
|
||||||
|
|
||||||
|
const followedCategories = user?.followedCategories ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
className={clsx(
|
||||||
|
'mr-2 items-center space-x-2 space-y-2 overflow-x-scroll scroll-smooth pt-4 pb-4 sm:flex-wrap',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<CategoryButton
|
||||||
|
key={'all' + followedCategories.length}
|
||||||
|
category="All"
|
||||||
|
isFollowed={followedCategories.length === 0}
|
||||||
|
toggle={async () => {
|
||||||
|
if (!user?.id) return
|
||||||
|
|
||||||
|
await updateUser(user.id, {
|
||||||
|
followedCategories: [],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{CATEGORY_LIST.map((cat) => (
|
||||||
|
<CategoryButton
|
||||||
|
key={cat + followedCategories.length}
|
||||||
|
category={CATEGORIES[cat].split(' ')[0]}
|
||||||
|
isFollowed={followedCategories.includes(cat)}
|
||||||
|
toggle={async () => {
|
||||||
|
if (!user?.id) return
|
||||||
|
|
||||||
|
await updateUser(user.id, {
|
||||||
|
followedCategories: [cat],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategoryButton(props: {
|
||||||
|
category: string
|
||||||
|
isFollowed: boolean
|
||||||
|
toggle: () => void
|
||||||
|
}) {
|
||||||
|
const { toggle, category, isFollowed } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'rounded-full border-2 px-4 py-1 shadow-md',
|
||||||
|
'cursor-pointer',
|
||||||
|
isFollowed ? 'border-gray-300 bg-gray-300' : 'bg-white'
|
||||||
|
)}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-gray-500">{category}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from './activity-items'
|
} from './activity-items'
|
||||||
import { FeedItems } from './feed-items'
|
import { FeedItems } from './feed-items'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
import { useContract } from 'web/hooks/use-contract'
|
||||||
|
|
||||||
export function ContractActivity(props: {
|
export function ContractActivity(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -27,8 +28,9 @@ export function ContractActivity(props: {
|
||||||
className?: string
|
className?: string
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, mode, contractPath, className, betRowClassName } =
|
const { user, mode, contractPath, className, betRowClassName } = props
|
||||||
props
|
|
||||||
|
const contract = useContract(props.contract.id) ?? props.contract
|
||||||
|
|
||||||
const updatedComments =
|
const updatedComments =
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { CATEGORIES } from '../../common/categories'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
|
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
@ -6,6 +7,9 @@ import { SiteLink } from './site-link'
|
||||||
|
|
||||||
function Hashtag(props: { tag: string; noLink?: boolean }) {
|
function Hashtag(props: { tag: string; noLink?: boolean }) {
|
||||||
const { tag, noLink } = props
|
const { tag, noLink } = props
|
||||||
|
const category = CATEGORIES[tag.replace('#', '').toLowerCase()]
|
||||||
|
console.log(tag, category)
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -13,7 +17,7 @@ function Hashtag(props: { tag: string; noLink?: boolean }) {
|
||||||
!noLink && 'cursor-pointer'
|
!noLink && 'cursor-pointer'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-600">{tag}</span>
|
<span className="text-sm text-gray-600">{category ?? tag}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,11 +34,12 @@ export function TagsList(props: {
|
||||||
className?: string
|
className?: string
|
||||||
noLink?: boolean
|
noLink?: boolean
|
||||||
noLabel?: boolean
|
noLabel?: boolean
|
||||||
|
label?: string
|
||||||
}) {
|
}) {
|
||||||
const { tags, className, noLink, noLabel } = props
|
const { tags, className, noLink, noLabel, label } = props
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('flex-wrap items-center gap-2', className)}>
|
<Row className={clsx('flex-wrap items-center gap-2', className)}>
|
||||||
{!noLabel && <div className="mr-1 text-gray-500">Tags</div>}
|
{!noLabel && <div className="mr-1 text-gray-500">{label || 'Tags'}</div>}
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<Hashtag
|
<Hashtag
|
||||||
key={tag}
|
key={tag}
|
||||||
|
|
|
@ -1,26 +1,18 @@
|
||||||
import _ from 'lodash'
|
import _, { Dictionary } from 'lodash'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Bet } from 'common/bet'
|
import type { feed } from 'common/feed'
|
||||||
import { Comment } from 'common/comment'
|
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
||||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { getUserFeed } from 'web/lib/firebase/users'
|
import { getCategoryFeeds, getUserFeed } from 'web/lib/firebase/users'
|
||||||
import { useUpdatedContracts } from './use-contracts'
|
|
||||||
import {
|
import {
|
||||||
getRecentBetsAndComments,
|
getRecentBetsAndComments,
|
||||||
getTopWeeklyContracts,
|
getTopWeeklyContracts,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
|
|
||||||
type feed = {
|
|
||||||
contract: Contract
|
|
||||||
recentBets: Bet[]
|
|
||||||
recentComments: Comment[]
|
|
||||||
}[]
|
|
||||||
|
|
||||||
export const useAlgoFeed = (user: User | null | undefined) => {
|
export const useAlgoFeed = (user: User | null | undefined) => {
|
||||||
const [feed, setFeed] = useState<feed>()
|
const [feed, setFeed] = useState<feed>()
|
||||||
|
const [categoryFeeds, setCategoryFeeds] = useState<Dictionary<feed>>()
|
||||||
|
|
||||||
const getTime = useTimeSinceFirstRender()
|
const getTime = useTimeSinceFirstRender()
|
||||||
|
|
||||||
|
@ -34,21 +26,21 @@ export const useAlgoFeed = (user: User | null | undefined) => {
|
||||||
trackLatency('feed', getTime())
|
trackLatency('feed', getTime())
|
||||||
console.log('feed load time', getTime())
|
console.log('feed load time', getTime())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
getCategoryFeeds(user.id).then((feeds) => {
|
||||||
|
setCategoryFeeds(feeds)
|
||||||
|
console.log('category feeds load time', getTime())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user?.id])
|
}, [user?.id])
|
||||||
|
|
||||||
return useUpdateFeed(feed)
|
const followedCategory = user?.followedCategories?.[0] ?? 'all'
|
||||||
}
|
|
||||||
|
|
||||||
const useUpdateFeed = (feed: feed | undefined) => {
|
const followedFeed =
|
||||||
const contracts = useUpdatedContracts(feed?.map((item) => item.contract))
|
followedCategory === 'all' ? feed : categoryFeeds?.[followedCategory]
|
||||||
|
|
||||||
return feed && contracts
|
return followedFeed
|
||||||
? feed.map(({ contract, ...other }, i) => ({
|
|
||||||
...other,
|
|
||||||
contract: contracts[i],
|
|
||||||
}))
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultFeed = async () => {
|
const getDefaultFeed = async () => {
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Contract, listenForContract } from 'web/lib/firebase/contracts'
|
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
||||||
|
import {
|
||||||
|
Contract,
|
||||||
|
contractDocRef,
|
||||||
|
listenForContract,
|
||||||
|
} from 'web/lib/firebase/contracts'
|
||||||
import { useStateCheckEquality } from './use-state-check-equality'
|
import { useStateCheckEquality } from './use-state-check-equality'
|
||||||
|
import { DocumentData } from 'firebase/firestore'
|
||||||
|
|
||||||
export const useContract = (contractId: string) => {
|
export const useContract = (contractId: string) => {
|
||||||
const [contract, setContract] = useState<Contract | null | 'loading'>(
|
const result = useFirestoreDocumentData<DocumentData, Contract>(
|
||||||
'loading'
|
['contracts', contractId],
|
||||||
|
contractDocRef(contractId),
|
||||||
|
{ subscribe: true, includeMetadataChanges: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
return result.isLoading ? undefined : result.data
|
||||||
if (contractId) return listenForContract(contractId, setContract)
|
|
||||||
}, [contractId])
|
|
||||||
|
|
||||||
return contract
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useContractWithPreload = (initial: Contract | null) => {
|
export const useContractWithPreload = (initial: Contract | null) => {
|
||||||
|
|
|
@ -79,6 +79,8 @@ export function tradingAllowed(contract: Contract) {
|
||||||
|
|
||||||
const db = getFirestore(app)
|
const db = getFirestore(app)
|
||||||
export const contractCollection = collection(db, 'contracts')
|
export const contractCollection = collection(db, 'contracts')
|
||||||
|
export const contractDocRef = (contractId: string) =>
|
||||||
|
doc(db, 'contracts', contractId)
|
||||||
|
|
||||||
// Push contract to Firestore
|
// Push contract to Firestore
|
||||||
export async function setContract(contract: Contract) {
|
export async function setContract(contract: Contract) {
|
||||||
|
|
|
@ -25,9 +25,8 @@ import { PrivateUser, User } from 'common/user'
|
||||||
import { createUser } from './api-call'
|
import { createUser } from './api-call'
|
||||||
import { getValue, getValues, listenForValue, listenForValues } from './utils'
|
import { getValue, getValues, listenForValue, listenForValues } from './utils'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { Contract } from './contracts'
|
import { feed } from 'common/feed'
|
||||||
import { Bet } from './bets'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { Comment } from './comments'
|
|
||||||
|
|
||||||
export type { User }
|
export type { User }
|
||||||
|
|
||||||
|
@ -216,11 +215,18 @@ export async function getDailyNewUsers(
|
||||||
export async function getUserFeed(userId: string) {
|
export async function getUserFeed(userId: string) {
|
||||||
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
|
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
|
||||||
const userFeed = await getValue<{
|
const userFeed = await getValue<{
|
||||||
feed: {
|
feed: feed
|
||||||
contract: Contract
|
|
||||||
recentBets: Bet[]
|
|
||||||
recentComments: Comment[]
|
|
||||||
}[]
|
|
||||||
}>(feedDoc)
|
}>(feedDoc)
|
||||||
return userFeed?.feed ?? []
|
return userFeed?.feed ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCategoryFeeds(userId: string) {
|
||||||
|
const cacheCollection = collection(db, 'private-users', userId, 'cache')
|
||||||
|
const feedData = await Promise.all(
|
||||||
|
CATEGORY_LIST.map((category) =>
|
||||||
|
getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const feeds = feedData.map((data) => data?.feed ?? [])
|
||||||
|
return _.fromPairs(_.zip(CATEGORY_LIST, feeds) as [string, feed][])
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Script from 'next/script'
|
import Script from 'next/script'
|
||||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
|
|
||||||
function firstLine(msg: string) {
|
function firstLine(msg: string) {
|
||||||
return msg.replace(/\r?\n.*/s, '')
|
return msg.replace(/\r?\n.*/s, '')
|
||||||
|
@ -76,9 +77,14 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</QueryClientProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
export default MyApp
|
export default MyApp
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { Row } from 'web/components/layout/row'
|
||||||
import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract'
|
import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
|
import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
|
||||||
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
import { CATEGORIES, CATEGORY_LIST, TO_CATEGORY } from 'common/categories'
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
@ -67,8 +69,10 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||||
const [initialProb, setInitialProb] = useState(50)
|
const [initialProb, setInitialProb] = useState(50)
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [tagText, setTagText] = useState<string>(tag ?? '')
|
|
||||||
const tags = parseWordsAsTags(tagText)
|
const [category, setCategory] = useState<string>('')
|
||||||
|
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||||
|
// const tags = parseWordsAsTags(tagText)
|
||||||
|
|
||||||
const [ante, setAnte] = useState(FIXED_ANTE)
|
const [ante, setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
|
@ -110,15 +114,17 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const result: any = await createContract({
|
const result: any = await createContract(
|
||||||
|
removeUndefinedProps({
|
||||||
question,
|
question,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb,
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags,
|
tags: category ? [category] : undefined,
|
||||||
}).then((r) => r.data || {})
|
})
|
||||||
|
).then((r) => r.data || {})
|
||||||
|
|
||||||
if (result.status !== 'success') {
|
if (result.status !== 'success') {
|
||||||
console.log('error creating contract', result)
|
console.log('error creating contract', result)
|
||||||
|
@ -199,25 +205,29 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<div className="form-control max-w-sm items-start">
|
<div className="form-control max-w-sm items-start">
|
||||||
<label className="label gap-2">
|
<label className="label gap-2">
|
||||||
<span className="mb-1">Tags</span>
|
<span className="mb-1">Category</span>
|
||||||
<InfoTooltip text="Optional. Help categorize your market with related tags." />
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<select
|
||||||
placeholder="e.g. Politics, Economics..."
|
className="select select-bordered w-full max-w-xs"
|
||||||
className="input input-bordered resize-none"
|
onChange={(e) =>
|
||||||
disabled={isSubmitting}
|
setCategory(TO_CATEGORY[e.currentTarget.value] ?? '')
|
||||||
value={tagText}
|
}
|
||||||
onChange={(e) => setTagText(e.target.value || '')}
|
>
|
||||||
/>
|
<option selected={category === ''}></option>
|
||||||
</div> */}
|
|
||||||
|
{CATEGORY_LIST.map((cat) => (
|
||||||
|
<option selected={category === cat} value={CATEGORIES[cat]}>
|
||||||
|
{CATEGORIES[cat]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Spacer h={4} />
|
|
||||||
<TagsList tags={tags} noLink noLabel />
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { useAlgoFeed } from 'web/hooks/use-algo-feed'
|
import { useAlgoFeed } from 'web/hooks/use-algo-feed'
|
||||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
import { ContractPageContent } from './[username]/[contractSlug]'
|
||||||
|
import { CategorySelector } from '../components/feed/category-selector'
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -42,7 +43,12 @@ const Home = () => {
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-[700px]">
|
<Col className="w-full max-w-[700px]">
|
||||||
<FeedCreate user={user ?? undefined} />
|
<FeedCreate user={user ?? undefined} />
|
||||||
<Spacer h={10} />
|
<Spacer h={2} />
|
||||||
|
|
||||||
|
<CategorySelector user={user} />
|
||||||
|
|
||||||
|
<Spacer h={1} />
|
||||||
|
|
||||||
{feed ? (
|
{feed ? (
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
feed={feed}
|
feed={feed}
|
||||||
|
|
85
yarn.lock
85
yarn.lock
|
@ -140,7 +140,7 @@
|
||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2":
|
||||||
version "7.17.9"
|
version "7.17.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||||
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||||
|
@ -958,6 +958,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
||||||
|
|
||||||
|
"@react-query-firebase/firestore@^0.4.2":
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-query-firebase/firestore/-/firestore-0.4.2.tgz#6ae52768715aa0a5c0d903dd4fd953ed417ba635"
|
||||||
|
integrity sha512-7eYp905+sfBRcBTdj7W7BAc3bI3V0D0kKca4/juOTnN4gyoNyaCNOCjLPY467dTq325hGs7BX0ol7Pw3JENdHA==
|
||||||
|
|
||||||
"@react-spring/animated@~9.2.6-beta.0":
|
"@react-spring/animated@~9.2.6-beta.0":
|
||||||
version "9.2.6"
|
version "9.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e"
|
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e"
|
||||||
|
@ -1535,6 +1540,11 @@ base64-js@^1.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
|
big-integer@^1.6.16:
|
||||||
|
version "1.6.51"
|
||||||
|
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
|
||||||
|
integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
|
||||||
|
|
||||||
bignumber.js@^9.0.0:
|
bignumber.js@^9.0.0:
|
||||||
version "9.0.2"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
|
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
|
||||||
|
@ -1588,6 +1598,20 @@ braces@^3.0.1, braces@~3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
broadcast-channel@^3.4.1:
|
||||||
|
version "3.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937"
|
||||||
|
integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.7.2"
|
||||||
|
detect-node "^2.1.0"
|
||||||
|
js-sha3 "0.8.0"
|
||||||
|
microseconds "0.2.0"
|
||||||
|
nano-time "1.0.0"
|
||||||
|
oblivious-set "1.0.0"
|
||||||
|
rimraf "3.0.2"
|
||||||
|
unload "2.2.0"
|
||||||
|
|
||||||
browserslist@^4.16.6:
|
browserslist@^4.16.6:
|
||||||
version "4.19.1"
|
version "4.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
|
||||||
|
@ -2128,6 +2152,11 @@ destroy@~1.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||||
|
|
||||||
|
detect-node@^2.0.4, detect-node@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||||
|
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||||
|
|
||||||
detective@^5.2.0:
|
detective@^5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
|
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
|
||||||
|
@ -3475,6 +3504,11 @@ jose@^2.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@panva/asn1.js" "^1.0.0"
|
"@panva/asn1.js" "^1.0.0"
|
||||||
|
|
||||||
|
js-sha3@0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||||
|
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
@ -3827,6 +3861,14 @@ make-dir@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver "^6.0.0"
|
semver "^6.0.0"
|
||||||
|
|
||||||
|
match-sorter@^6.0.2:
|
||||||
|
version "6.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
|
||||||
|
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
remove-accents "0.4.2"
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
|
@ -3860,6 +3902,11 @@ micromatch@^4.0.4:
|
||||||
braces "^3.0.1"
|
braces "^3.0.1"
|
||||||
picomatch "^2.2.3"
|
picomatch "^2.2.3"
|
||||||
|
|
||||||
|
microseconds@0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
|
||||||
|
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
|
||||||
|
|
||||||
mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
|
mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
|
||||||
version "1.51.0"
|
version "1.51.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
|
||||||
|
@ -3957,6 +4004,13 @@ multimatch@^4.0.0:
|
||||||
arrify "^2.0.1"
|
arrify "^2.0.1"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
|
|
||||||
|
nano-time@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
|
||||||
|
integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
|
||||||
|
dependencies:
|
||||||
|
big-integer "^1.6.16"
|
||||||
|
|
||||||
nanoid@^3.1.23, nanoid@^3.2.0:
|
nanoid@^3.1.23, nanoid@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
|
||||||
|
@ -4139,6 +4193,11 @@ object.values@^1.1.5:
|
||||||
define-properties "^1.1.3"
|
define-properties "^1.1.3"
|
||||||
es-abstract "^1.19.1"
|
es-abstract "^1.19.1"
|
||||||
|
|
||||||
|
oblivious-set@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
|
||||||
|
integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
|
||||||
|
|
||||||
on-finished@~2.3.0:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
|
@ -4695,6 +4754,15 @@ react-motion@^0.5.2:
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
raf "^3.1.0"
|
raf "^3.1.0"
|
||||||
|
|
||||||
|
react-query@^3.39.0:
|
||||||
|
version "3.39.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050"
|
||||||
|
integrity sha512-Od0IkSuS79WJOhzWBx/ys0x13+7wFqgnn64vBqqAAnZ9whocVhl/y1padD5uuZ6EIkXbFbInax0qvY7zGM0thA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
broadcast-channel "^3.4.1"
|
||||||
|
match-sorter "^6.0.2"
|
||||||
|
|
||||||
react-with-forwarded-ref@^0.3.3:
|
react-with-forwarded-ref@^0.3.3:
|
||||||
version "0.3.4"
|
version "0.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
|
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
|
||||||
|
@ -4767,6 +4835,11 @@ regexpp@^3.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
|
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
|
||||||
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
||||||
|
|
||||||
|
remove-accents@0.4.2:
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||||
|
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
|
||||||
|
|
||||||
require-directory@^2.1.1:
|
require-directory@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
|
@ -4830,7 +4903,7 @@ rfdc@^1.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
||||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
||||||
|
|
||||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
@ -5421,6 +5494,14 @@ unique-string@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string "^2.0.0"
|
crypto-random-string "^2.0.0"
|
||||||
|
|
||||||
|
unload@2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
|
||||||
|
integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.6.2"
|
||||||
|
detect-node "^2.0.4"
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user