Categories to groups (#641)
* start on script
* Revert "Remove category filters"
This reverts commit d6e808e1a3
.
* Convert categories to official default groups
* Add new users to default groups
* Rework group cards
* Cleanup
* Add unique bettors to contract and sort by them
* Most bettors to most popular
* Unused vars
* Track unique bettor ids on contracts
* Add followed users' bets to personal markets
* Add new users to welcome, bugs, and updates groups
* Add users to fewer default cats
This commit is contained in:
parent
e868f0a15a
commit
55c91dfcdd
|
@ -2,7 +2,7 @@ import { cloneDeep, range, sum, sumBy, sortBy, mapValues } from 'lodash'
|
||||||
import { Bet, NumericBet } from './bet'
|
import { Bet, NumericBet } from './bet'
|
||||||
import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
|
import { DPMContract, DPMBinaryContract, NumericContract } from './contract'
|
||||||
import { DPM_FEES } from './fees'
|
import { DPM_FEES } from './fees'
|
||||||
import { normpdf } from '../common/util/math'
|
import { normpdf } from './util/math'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
|
|
||||||
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
export function getDpmProbability(totalShares: { [outcome: string]: number }) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { difference } from 'lodash'
|
import { difference } from 'lodash'
|
||||||
|
|
||||||
|
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
|
||||||
export const CATEGORIES = {
|
export const CATEGORIES = {
|
||||||
politics: 'Politics',
|
politics: 'Politics',
|
||||||
technology: 'Technology',
|
technology: 'Technology',
|
||||||
|
@ -24,9 +25,15 @@ export const TO_CATEGORY = Object.fromEntries(
|
||||||
|
|
||||||
export const CATEGORY_LIST = Object.keys(CATEGORIES)
|
export const CATEGORY_LIST = Object.keys(CATEGORIES)
|
||||||
|
|
||||||
export const EXCLUDED_CATEGORIES: category[] = ['fun', 'manifold', 'personal']
|
export const EXCLUDED_CATEGORIES: category[] = [
|
||||||
|
'fun',
|
||||||
|
'manifold',
|
||||||
|
'personal',
|
||||||
|
'covid',
|
||||||
|
'culture',
|
||||||
|
'gaming',
|
||||||
|
'crypto',
|
||||||
|
'world',
|
||||||
|
]
|
||||||
|
|
||||||
export const DEFAULT_CATEGORIES = difference(
|
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
|
||||||
CATEGORY_LIST,
|
|
||||||
EXCLUDED_CATEGORIES
|
|
||||||
)
|
|
||||||
|
|
|
@ -44,6 +44,10 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
volume7Days: number
|
volume7Days: number
|
||||||
|
|
||||||
collectedFees: Fees
|
collectedFees: Fees
|
||||||
|
|
||||||
|
groupSlugs?: string[]
|
||||||
|
uniqueBettorIds?: string[]
|
||||||
|
uniqueBettorCount?: number
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
export type BinaryContract = Contract & Binary
|
export type BinaryContract = Contract & Binary
|
||||||
|
@ -109,7 +113,12 @@ export type Numeric = {
|
||||||
export type outcomeType = AnyOutcomeType['outcomeType']
|
export type outcomeType = AnyOutcomeType['outcomeType']
|
||||||
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
||||||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||||
export const OUTCOME_TYPES = ['BINARY', 'FREE_RESPONSE', 'PSEUDO_NUMERIC', 'NUMERIC'] as const
|
export const OUTCOME_TYPES = [
|
||||||
|
'BINARY',
|
||||||
|
'FREE_RESPONSE',
|
||||||
|
'PSEUDO_NUMERIC',
|
||||||
|
'NUMERIC',
|
||||||
|
] as const
|
||||||
|
|
||||||
export const MAX_QUESTION_LENGTH = 480
|
export const MAX_QUESTION_LENGTH = 480
|
||||||
export const MAX_DESCRIPTION_LENGTH = 10000
|
export const MAX_DESCRIPTION_LENGTH = 10000
|
||||||
|
|
|
@ -9,7 +9,10 @@ export type Group = {
|
||||||
memberIds: string[] // User ids
|
memberIds: string[] // User ids
|
||||||
anyoneCanJoin: boolean
|
anyoneCanJoin: boolean
|
||||||
contractIds: string[]
|
contractIds: string[]
|
||||||
|
|
||||||
|
chatDisabled?: boolean
|
||||||
}
|
}
|
||||||
export const MAX_GROUP_NAME_LENGTH = 75
|
export const MAX_GROUP_NAME_LENGTH = 75
|
||||||
export const MAX_ABOUT_LENGTH = 140
|
export const MAX_ABOUT_LENGTH = 140
|
||||||
export const MAX_ID_LENGTH = 60
|
export const MAX_ID_LENGTH = 60
|
||||||
|
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
|
||||||
|
|
|
@ -71,7 +71,7 @@ service cloud.firestore {
|
||||||
match /contracts/{contractId} {
|
match /contracts/{contractId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['tags', 'lowercaseTags']);
|
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs']);
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['description', 'closeTime'])
|
.hasOnly(['description', 'closeTime'])
|
||||||
&& resource.data.creatorId == request.auth.uid;
|
&& resource.data.creatorId == request.auth.uid;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
SUS_STARTING_BALANCE,
|
SUS_STARTING_BALANCE,
|
||||||
User,
|
User,
|
||||||
} from '../../common/user'
|
} from '../../common/user'
|
||||||
import { getUser, getUserByUsername } from './utils'
|
import { getUser, getUserByUsername, getValues, isProd } from './utils'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import {
|
import {
|
||||||
cleanDisplayName,
|
cleanDisplayName,
|
||||||
|
@ -14,10 +14,19 @@ import {
|
||||||
} from '../../common/util/clean-username'
|
} from '../../common/util/clean-username'
|
||||||
import { sendWelcomeEmail } from './emails'
|
import { sendWelcomeEmail } from './emails'
|
||||||
import { isWhitelisted } from '../../common/envs/constants'
|
import { isWhitelisted } from '../../common/envs/constants'
|
||||||
import { DEFAULT_CATEGORIES } from '../../common/categories'
|
import {
|
||||||
|
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
|
} from '../../common/categories'
|
||||||
|
|
||||||
import { track } from './analytics'
|
import { track } from './analytics'
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
|
import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import {
|
||||||
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
} from '../../common/antes'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
deviceToken: z.string().optional(),
|
deviceToken: z.string().optional(),
|
||||||
|
@ -85,7 +94,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||||
|
|
||||||
await sendWelcomeEmail(user, privateUser)
|
await sendWelcomeEmail(user, privateUser)
|
||||||
|
await addUserToDefaultGroups(user)
|
||||||
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
await track(auth.uid, 'create user', { username }, { ip: req.ip })
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -110,3 +119,50 @@ const numberUsersWithIp = async (ipAddress: string) => {
|
||||||
|
|
||||||
return snap.docs.length
|
return snap.docs.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addUserToDefaultGroups = async (user: User) => {
|
||||||
|
for (const category of Object.values(DEFAULT_CATEGORIES)) {
|
||||||
|
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
|
||||||
|
const groups = await getValues<Group>(
|
||||||
|
firestore.collection('groups').where('slug', '==', slug)
|
||||||
|
)
|
||||||
|
await firestore
|
||||||
|
.collection('groups')
|
||||||
|
.doc(groups[0].id)
|
||||||
|
.update({
|
||||||
|
memberIds: uniq(groups[0].memberIds.concat(user.id)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const slug of NEW_USER_GROUP_SLUGS) {
|
||||||
|
const groups = await getValues<Group>(
|
||||||
|
firestore.collection('groups').where('slug', '==', slug)
|
||||||
|
)
|
||||||
|
const group = groups[0]
|
||||||
|
await firestore
|
||||||
|
.collection('groups')
|
||||||
|
.doc(group.id)
|
||||||
|
.update({
|
||||||
|
memberIds: uniq(group.memberIds.concat(user.id)),
|
||||||
|
})
|
||||||
|
const manifoldAccount = isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
|
||||||
|
if (slug === 'welcome') {
|
||||||
|
const welcomeCommentDoc = firestore
|
||||||
|
.collection(`groups/${group.id}/comments`)
|
||||||
|
.doc()
|
||||||
|
await welcomeCommentDoc.create({
|
||||||
|
id: welcomeCommentDoc.id,
|
||||||
|
groupId: group.id,
|
||||||
|
userId: manifoldAccount,
|
||||||
|
text: `Welcome, ${user.name} (@${user.username})!`,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
userName: 'Manifold Markets',
|
||||||
|
userUsername: 'ManifoldMarkets',
|
||||||
|
userAvatarUrl: 'https://manifold.markets/logo-bg-white.png',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
import { APIError, newEndpoint } from './api'
|
|
||||||
import { isProd, log } from './utils'
|
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
import { PrivateUser } from '../../common/lib/user'
|
|
||||||
import { uniq } from 'lodash'
|
|
||||||
import { Bet } from '../../common/lib/bet'
|
|
||||||
const firestore = admin.firestore()
|
|
||||||
import {
|
|
||||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
||||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
||||||
} from '../../common/antes'
|
|
||||||
import { runTxn, TxnData } from './transact'
|
|
||||||
import { createNotification } from './create-notification'
|
|
||||||
import { User } from '../../common/lib/user'
|
|
||||||
import { Contract } from '../../common/lib/contract'
|
|
||||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
|
||||||
|
|
||||||
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
|
|
||||||
const QUERY_LIMIT_SECONDS = 60
|
|
||||||
|
|
||||||
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|
||||||
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
|
|
||||||
async (trans) => {
|
|
||||||
const userSnap = await trans.get(
|
|
||||||
firestore.doc(`private-users/${auth.uid}`)
|
|
||||||
)
|
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
|
||||||
const user = userSnap.data() as PrivateUser
|
|
||||||
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
|
|
||||||
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
|
|
||||||
throw new APIError(
|
|
||||||
400,
|
|
||||||
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
|
|
||||||
)
|
|
||||||
await trans.update(userSnap.ref, {
|
|
||||||
lastTimeCheckedBonuses: Date.now(),
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
lastTimeCheckedBonuses,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const fromUserId = isProd()
|
|
||||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
|
||||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
|
||||||
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
|
||||||
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
|
||||||
const fromUser = fromSnap.data() as User
|
|
||||||
// Get all users contracts made since implementation time
|
|
||||||
const userContractsSnap = await firestore
|
|
||||||
.collection(`contracts`)
|
|
||||||
.where('creatorId', '==', user.id)
|
|
||||||
.where('createdTime', '>=', BONUS_START_DATE)
|
|
||||||
.get()
|
|
||||||
const userContracts = userContractsSnap.docs.map(
|
|
||||||
(doc) => doc.data() as Contract
|
|
||||||
)
|
|
||||||
const nullReturn = { status: 'no bets', txn: null }
|
|
||||||
for (const contract of userContracts) {
|
|
||||||
const result = await firestore.runTransaction(async (trans) => {
|
|
||||||
const contractId = contract.id
|
|
||||||
// Get all bets made on user's contracts
|
|
||||||
const bets = (
|
|
||||||
await firestore
|
|
||||||
.collection(`contracts/${contractId}/bets`)
|
|
||||||
.where('userId', '!=', user.id)
|
|
||||||
.get()
|
|
||||||
).docs.map((bet) => bet.ref)
|
|
||||||
if (bets.length === 0) {
|
|
||||||
return nullReturn
|
|
||||||
}
|
|
||||||
const contractBetsSnap = await trans.getAll(...bets)
|
|
||||||
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
|
||||||
|
|
||||||
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
|
||||||
contractBets
|
|
||||||
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
|
|
||||||
.map((bet) => bet.userId)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Filter users for ONLY those that have made bets since the last daily bonus received time
|
|
||||||
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
|
|
||||||
contractBets
|
|
||||||
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
|
|
||||||
.map((bet) => bet.userId)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Filter for users only present in the above list
|
|
||||||
const newUniqueBettorIds =
|
|
||||||
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
|
||||||
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
|
||||||
)
|
|
||||||
newUniqueBettorIds.length > 0 &&
|
|
||||||
log(
|
|
||||||
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
|
||||||
)
|
|
||||||
if (newUniqueBettorIds.length === 0) {
|
|
||||||
return nullReturn
|
|
||||||
}
|
|
||||||
// Create combined txn for all unique bettors
|
|
||||||
const bonusTxnDetails = {
|
|
||||||
contractId: contractId,
|
|
||||||
uniqueBettors: newUniqueBettorIds.length,
|
|
||||||
}
|
|
||||||
const bonusTxn: TxnData = {
|
|
||||||
fromId: fromUser.id,
|
|
||||||
fromType: 'BANK',
|
|
||||||
toId: user.id,
|
|
||||||
toType: 'USER',
|
|
||||||
amount: UNIQUE_BETTOR_BONUS_AMOUNT * newUniqueBettorIds.length,
|
|
||||||
token: 'M$',
|
|
||||||
category: 'UNIQUE_BETTOR_BONUS',
|
|
||||||
description: JSON.stringify(bonusTxnDetails),
|
|
||||||
}
|
|
||||||
return await runTxn(trans, bonusTxn)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status != 'success' || !result.txn) {
|
|
||||||
result.status != nullReturn.status &&
|
|
||||||
log(`No bonus for user: ${user.id} - reason:`, result.status)
|
|
||||||
} else {
|
|
||||||
log(`Bonus txn for user: ${user.id} completed:`, result.txn?.id)
|
|
||||||
await createNotification(
|
|
||||||
result.txn.id,
|
|
||||||
'bonus',
|
|
||||||
'created',
|
|
||||||
fromUser,
|
|
||||||
result.txn.id,
|
|
||||||
result.txn.amount + '',
|
|
||||||
contract,
|
|
||||||
undefined,
|
|
||||||
// No need to set the user id, we'll use the contract creator id
|
|
||||||
undefined,
|
|
||||||
contract.slug,
|
|
||||||
contract.question
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { userId: user.id, message: 'success' }
|
|
||||||
})
|
|
|
@ -38,6 +38,5 @@ export * from './add-liquidity'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
export * from './create-group'
|
export * from './create-group'
|
||||||
export * from './resolve-market'
|
export * from './resolve-market'
|
||||||
export * from './get-daily-bonuses'
|
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
export * from './stripe'
|
export * from './stripe'
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
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 { keyBy } from 'lodash'
|
import { keyBy, uniq } from 'lodash'
|
||||||
|
|
||||||
import { Bet, LimitBet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
import { getContract, getUser, getValues } from './utils'
|
import { getContract, getUser, getValues, isProd, log } from './utils'
|
||||||
import { createBetFillNotification } from './create-notification'
|
import {
|
||||||
|
createBetFillNotification,
|
||||||
|
createNotification,
|
||||||
|
} from './create-notification'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import { runTxn, TxnData } from './transact'
|
||||||
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
||||||
|
import {
|
||||||
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
} from '../../common/antes'
|
||||||
|
import { APIError } from '../../common/api'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||||
|
|
||||||
export const onCreateBet = functions.firestore
|
export const onCreateBet = functions.firestore
|
||||||
.document('contracts/{contractId}/bets/{betId}')
|
.document('contracts/{contractId}/bets/{betId}')
|
||||||
|
@ -26,8 +39,109 @@ export const onCreateBet = functions.firestore
|
||||||
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
||||||
|
|
||||||
await notifyFills(bet, contractId, eventId)
|
await notifyFills(bet, contractId, eventId)
|
||||||
|
await updateUniqueBettorsAndGiveCreatorBonus(
|
||||||
|
contractId,
|
||||||
|
eventId,
|
||||||
|
bet.userId
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
|
contractId: string,
|
||||||
|
eventId: string,
|
||||||
|
bettorId: string
|
||||||
|
) => {
|
||||||
|
const userContractSnap = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.doc(contractId)
|
||||||
|
.get()
|
||||||
|
const contract = userContractSnap.data() as Contract
|
||||||
|
if (!contract) {
|
||||||
|
log(`Could not find contract ${contractId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let previousUniqueBettorIds = contract.uniqueBettorIds
|
||||||
|
|
||||||
|
if (!previousUniqueBettorIds) {
|
||||||
|
const contractBets = (
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.where('userId', '!=', contract.creatorId)
|
||||||
|
.get()
|
||||||
|
).docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
|
if (contractBets.length === 0) {
|
||||||
|
log(`No bets for contract ${contractId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previousUniqueBettorIds = uniq(
|
||||||
|
contractBets
|
||||||
|
.filter((bet) => bet.createdTime < BONUS_START_DATE)
|
||||||
|
.map((bet) => bet.userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId)
|
||||||
|
|
||||||
|
const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId])
|
||||||
|
// Update contract unique bettors
|
||||||
|
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
||||||
|
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
||||||
|
isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`)
|
||||||
|
await firestore.collection(`contracts`).doc(contractId).update({
|
||||||
|
uniqueBettorIds: newUniqueBettorIds,
|
||||||
|
uniqueBettorCount: newUniqueBettorIds.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!isNewUniqueBettor) return
|
||||||
|
|
||||||
|
// Create combined txn for all new unique bettors
|
||||||
|
const bonusTxnDetails = {
|
||||||
|
contractId: contractId,
|
||||||
|
uniqueBettorIds: newUniqueBettorIds,
|
||||||
|
}
|
||||||
|
const fromUserId = isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||||
|
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||||
|
const fromUser = fromSnap.data() as User
|
||||||
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
|
const bonusTxn: TxnData = {
|
||||||
|
fromId: fromUser.id,
|
||||||
|
fromType: 'BANK',
|
||||||
|
toId: contract.creatorId,
|
||||||
|
toType: 'USER',
|
||||||
|
amount: UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||||
|
token: 'M$',
|
||||||
|
category: 'UNIQUE_BETTOR_BONUS',
|
||||||
|
description: JSON.stringify(bonusTxnDetails),
|
||||||
|
}
|
||||||
|
return await runTxn(trans, bonusTxn)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.status != 'success' || !result.txn) {
|
||||||
|
log(`No bonus for user: ${contract.creatorId} - reason:`, result.status)
|
||||||
|
} else {
|
||||||
|
log(`Bonus txn for user: ${contract.creatorId} completed:`, result.txn?.id)
|
||||||
|
await createNotification(
|
||||||
|
result.txn.id,
|
||||||
|
'bonus',
|
||||||
|
'created',
|
||||||
|
fromUser,
|
||||||
|
eventId + '-bonus',
|
||||||
|
result.txn.amount + '',
|
||||||
|
contract,
|
||||||
|
undefined,
|
||||||
|
// No need to set the user id, we'll use the contract creator id
|
||||||
|
undefined,
|
||||||
|
contract.slug,
|
||||||
|
contract.question
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
||||||
if (!bet.fills) return
|
if (!bet.fills) return
|
||||||
|
|
||||||
|
|
110
functions/src/scripts/convert-categories.ts
Normal file
110
functions/src/scripts/convert-categories.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
initAdmin()
|
||||||
|
|
||||||
|
import { getValues, isProd } from '../utils'
|
||||||
|
import {
|
||||||
|
CATEGORIES_GROUP_SLUG_POSTFIX,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
|
} from 'common/categories'
|
||||||
|
import { Group } from 'common/group'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { filterDefined } from 'common/util/array'
|
||||||
|
import {
|
||||||
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
} from 'common/antes'
|
||||||
|
|
||||||
|
const adminFirestore = admin.firestore()
|
||||||
|
|
||||||
|
async function convertCategoriesToGroups() {
|
||||||
|
const groups = await getValues<Group>(adminFirestore.collection('groups'))
|
||||||
|
const contracts = await getValues<Contract>(
|
||||||
|
adminFirestore.collection('contracts')
|
||||||
|
)
|
||||||
|
for (const group of groups) {
|
||||||
|
const groupContracts = contracts.filter((contract) =>
|
||||||
|
group.contractIds.includes(contract.id)
|
||||||
|
)
|
||||||
|
for (const contract of groupContracts) {
|
||||||
|
await adminFirestore
|
||||||
|
.collection('contracts')
|
||||||
|
.doc(contract.id)
|
||||||
|
.update({
|
||||||
|
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const category of Object.values(DEFAULT_CATEGORIES)) {
|
||||||
|
const markets = await getValues<Contract>(
|
||||||
|
adminFirestore
|
||||||
|
.collection('contracts')
|
||||||
|
.where('lowercaseTags', 'array-contains', category.toLowerCase())
|
||||||
|
)
|
||||||
|
const slug = category.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX
|
||||||
|
const oldGroup = await getValues<Group>(
|
||||||
|
adminFirestore.collection('groups').where('slug', '==', slug)
|
||||||
|
)
|
||||||
|
if (oldGroup.length > 0) {
|
||||||
|
console.log(`Found old group for ${category}`)
|
||||||
|
await adminFirestore.collection('groups').doc(oldGroup[0].id).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
const allUsers = await getValues<User>(adminFirestore.collection('users'))
|
||||||
|
const groupUsers = filterDefined(
|
||||||
|
allUsers.map((user: User) => {
|
||||||
|
if (!user.followedCategories || user.followedCategories.length === 0)
|
||||||
|
return user.id
|
||||||
|
if (!user.followedCategories.includes(category.toLowerCase()))
|
||||||
|
return null
|
||||||
|
return user.id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const manifoldAccount = isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
const newGroupRef = await adminFirestore.collection('groups').doc()
|
||||||
|
const newGroup: Group = {
|
||||||
|
id: newGroupRef.id,
|
||||||
|
name: category,
|
||||||
|
slug,
|
||||||
|
creatorId: manifoldAccount,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
anyoneCanJoin: true,
|
||||||
|
memberIds: [manifoldAccount],
|
||||||
|
about: 'Official group for all things related to ' + category,
|
||||||
|
mostRecentActivityTime: Date.now(),
|
||||||
|
contractIds: markets.map((market) => market.id),
|
||||||
|
chatDisabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
await adminFirestore.collection('groups').doc(newGroupRef.id).set(newGroup)
|
||||||
|
// Update group with new memberIds to avoid notifying everyone
|
||||||
|
await adminFirestore
|
||||||
|
.collection('groups')
|
||||||
|
.doc(newGroupRef.id)
|
||||||
|
.update({
|
||||||
|
memberIds: uniq(groupUsers),
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const market of markets) {
|
||||||
|
await adminFirestore
|
||||||
|
.collection('contracts')
|
||||||
|
.doc(market.id)
|
||||||
|
.update({
|
||||||
|
groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
convertCategoriesToGroups()
|
||||||
|
.then(() => process.exit())
|
||||||
|
.catch(console.log)
|
||||||
|
}
|
|
@ -20,8 +20,12 @@ import { Row } from './layout/row'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
||||||
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
|
import { NEW_USER_GROUP_SLUGS } from 'common/group'
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
'GJQPAYENIF',
|
'GJQPAYENIF',
|
||||||
|
@ -33,6 +37,7 @@ const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
|
||||||
const sortIndexes = [
|
const sortIndexes = [
|
||||||
{ label: 'Newest', value: indexPrefix + 'contracts-newest' },
|
{ label: 'Newest', value: indexPrefix + 'contracts-newest' },
|
||||||
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
|
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
|
||||||
|
{ label: 'Most popular', value: indexPrefix + 'contracts-most-popular' },
|
||||||
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
|
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
|
||||||
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
|
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
|
||||||
{ label: 'Last updated', value: indexPrefix + 'contracts-last-updated' },
|
{ label: 'Last updated', value: indexPrefix + 'contracts-last-updated' },
|
||||||
|
@ -40,7 +45,7 @@ const sortIndexes = [
|
||||||
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
|
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
|
||||||
]
|
]
|
||||||
|
|
||||||
type filter = 'open' | 'closed' | 'resolved' | 'all'
|
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
|
||||||
export function ContractSearch(props: {
|
export function ContractSearch(props: {
|
||||||
querySortOptions?: {
|
querySortOptions?: {
|
||||||
|
@ -69,13 +74,19 @@ export function ContractSearch(props: {
|
||||||
hideQuickBet,
|
hideQuickBet,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const user = useUser()
|
||||||
|
const memberGroupSlugs = useMemberGroups(user?.id)
|
||||||
|
?.map((g) => g.slug)
|
||||||
|
.filter((s) => !NEW_USER_GROUP_SLUGS.includes(s))
|
||||||
|
const follows = useFollows(user?.id)
|
||||||
|
console.log(memberGroupSlugs, follows)
|
||||||
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
||||||
|
|
||||||
const sort = sortIndexes
|
const sort = sortIndexes
|
||||||
.map(({ value }) => value)
|
.map(({ value }) => value)
|
||||||
.includes(`${indexPrefix}contracts-${initialSort ?? ''}`)
|
.includes(`${indexPrefix}contracts-${initialSort ?? ''}`)
|
||||||
? initialSort
|
? initialSort
|
||||||
: querySortOptions?.defaultSort ?? '24-hour-vol'
|
: querySortOptions?.defaultSort ?? 'most-popular'
|
||||||
|
|
||||||
const [filter, setFilter] = useState<filter>(
|
const [filter, setFilter] = useState<filter>(
|
||||||
querySortOptions?.defaultFilter ?? 'open'
|
querySortOptions?.defaultFilter ?? 'open'
|
||||||
|
@ -86,10 +97,21 @@ export function ContractSearch(props: {
|
||||||
filter === 'open' ? 'isResolved:false' : '',
|
filter === 'open' ? 'isResolved:false' : '',
|
||||||
filter === 'closed' ? 'isResolved:false' : '',
|
filter === 'closed' ? 'isResolved:false' : '',
|
||||||
filter === 'resolved' ? 'isResolved:true' : '',
|
filter === 'resolved' ? 'isResolved:true' : '',
|
||||||
|
filter === 'personal'
|
||||||
|
? // Show contracts in groups that the user is a member of
|
||||||
|
(memberGroupSlugs?.map((slug) => `groupSlugs:${slug}`) ?? [])
|
||||||
|
// Show contracts created by users the user follows
|
||||||
|
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
||||||
|
// Show contracts bet on by users the user follows
|
||||||
|
.concat(
|
||||||
|
follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? []
|
||||||
|
// Show contracts bet on by the user
|
||||||
|
)
|
||||||
|
.concat(user ? `uniqueBettorIds:${user.id}` : [])
|
||||||
|
: '',
|
||||||
additionalFilter?.creatorId
|
additionalFilter?.creatorId
|
||||||
? `creatorId:${additionalFilter.creatorId}`
|
? `creatorId:${additionalFilter.creatorId}`
|
||||||
: '',
|
: '',
|
||||||
additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '',
|
|
||||||
].filter((f) => f)
|
].filter((f) => f)
|
||||||
// Hack to make Algolia work.
|
// Hack to make Algolia work.
|
||||||
filters = ['', ...filters]
|
filters = ['', ...filters]
|
||||||
|
@ -100,7 +122,12 @@ export function ContractSearch(props: {
|
||||||
].filter((f) => f)
|
].filter((f) => f)
|
||||||
|
|
||||||
return { filters, numericFilters }
|
return { filters, numericFilters }
|
||||||
}, [filter, Object.values(additionalFilter ?? {}).join(',')])
|
}, [
|
||||||
|
filter,
|
||||||
|
Object.values(additionalFilter ?? {}).join(','),
|
||||||
|
(memberGroupSlugs ?? []).join(','),
|
||||||
|
(follows ?? []).join(','),
|
||||||
|
])
|
||||||
|
|
||||||
const indexName = `${indexPrefix}contracts-${sort}`
|
const indexName = `${indexPrefix}contracts-${sort}`
|
||||||
|
|
||||||
|
@ -125,6 +152,7 @@ export function ContractSearch(props: {
|
||||||
resetIcon: 'mt-2 hidden sm:flex',
|
resetIcon: 'mt-2 hidden sm:flex',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/*// TODO track WHICH filter users are using*/}
|
||||||
<select
|
<select
|
||||||
className="!select !select-bordered"
|
className="!select !select-bordered"
|
||||||
value={filter}
|
value={filter}
|
||||||
|
@ -134,6 +162,7 @@ export function ContractSearch(props: {
|
||||||
<option value="open">Open</option>
|
<option value="open">Open</option>
|
||||||
<option value="closed">Closed</option>
|
<option value="closed">Closed</option>
|
||||||
<option value="resolved">Resolved</option>
|
<option value="resolved">Resolved</option>
|
||||||
|
<option value="personal">For you</option>
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
</select>
|
</select>
|
||||||
{!hideOrderSelector && (
|
{!hideOrderSelector && (
|
||||||
|
@ -155,13 +184,21 @@ export function ContractSearch(props: {
|
||||||
|
|
||||||
<Spacer h={3} />
|
<Spacer h={3} />
|
||||||
|
|
||||||
<ContractSearchInner
|
{/*<Spacer h={4} />*/}
|
||||||
querySortOptions={querySortOptions}
|
|
||||||
onContractClick={onContractClick}
|
{filter === 'personal' &&
|
||||||
overrideGridClassName={overrideGridClassName}
|
(follows ?? []).length === 0 &&
|
||||||
hideQuickBet={hideQuickBet}
|
(memberGroupSlugs ?? []).length === 0 ? (
|
||||||
excludeContractIds={additionalFilter?.excludeContractIds}
|
<>You're not following anyone, nor in any of your own groups yet.</>
|
||||||
/>
|
) : (
|
||||||
|
<ContractSearchInner
|
||||||
|
querySortOptions={querySortOptions}
|
||||||
|
onContractClick={onContractClick}
|
||||||
|
overrideGridClassName={overrideGridClassName}
|
||||||
|
hideQuickBet={hideQuickBet}
|
||||||
|
excludeContractIds={additionalFilter?.excludeContractIds}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</InstantSearch>
|
</InstantSearch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,12 @@ export function Leaderboard(props: {
|
||||||
renderCell: (user: User) => any
|
renderCell: (user: User) => any
|
||||||
}[]
|
}[]
|
||||||
className?: string
|
className?: string
|
||||||
|
maxToShow?: number
|
||||||
}) {
|
}) {
|
||||||
// TODO: Ideally, highlight your own entry on the leaderboard
|
// TODO: Ideally, highlight your own entry on the leaderboard
|
||||||
const { title, users, columns, className } = props
|
const { title, columns, className } = props
|
||||||
|
const maxToShow = props.maxToShow ?? props.users.length
|
||||||
|
const users = props.users.slice(0, maxToShow)
|
||||||
return (
|
return (
|
||||||
<div className={clsx('w-full px-1', className)}>
|
<div className={clsx('w-full px-1', className)}>
|
||||||
<Title text={title} className="!mt-0" />
|
<Title text={title} className="!mt-0" />
|
||||||
|
|
|
@ -193,7 +193,9 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
const mobileNavigationOptions = !user
|
const mobileNavigationOptions = !user
|
||||||
? signedOutMobileNavigation
|
? signedOutMobileNavigation
|
||||||
: signedInMobileNavigation
|
: signedInMobileNavigation
|
||||||
const memberItems = (useMemberGroups(user?.id) ?? []).map((group: Group) => ({
|
const memberItems = (
|
||||||
|
useMemberGroups(user?.id, { withChatEnabled: true }) ?? []
|
||||||
|
).map((group: Group) => ({
|
||||||
name: group.name,
|
name: group.name,
|
||||||
href: groupPath(group.slug),
|
href: groupPath(group.slug),
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -6,22 +6,12 @@ import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications'
|
import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications'
|
||||||
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
||||||
import { requestBonuses } from 'web/lib/firebase/api'
|
|
||||||
import { PrivateUser } from 'common/user'
|
import { PrivateUser } from 'common/user'
|
||||||
|
|
||||||
export default function NotificationsIcon(props: { className?: string }) {
|
export default function NotificationsIcon(props: { className?: string }) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const privateUser = usePrivateUser(user?.id)
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
privateUser &&
|
|
||||||
privateUser.lastTimeCheckedBonuses &&
|
|
||||||
Date.now() - privateUser.lastTimeCheckedBonuses > 1000 * 70
|
|
||||||
)
|
|
||||||
requestBonuses({}).catch(() => console.log('no bonuses for you (yet)'))
|
|
||||||
}, [privateUser])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('justify-center')}>
|
<Row className={clsx('justify-center')}>
|
||||||
<div className={'relative'}>
|
<div className={'relative'}>
|
||||||
|
|
|
@ -2,12 +2,15 @@ import { useEffect, useState } from 'react'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
|
getGroupBySlug,
|
||||||
getGroupsWithContractId,
|
getGroupsWithContractId,
|
||||||
listenForGroup,
|
listenForGroup,
|
||||||
listenForGroups,
|
listenForGroups,
|
||||||
listenForMemberGroups,
|
listenForMemberGroups,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { getUser } from 'web/lib/firebase/users'
|
import { getUser } from 'web/lib/firebase/users'
|
||||||
|
import { CATEGORIES, CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories'
|
||||||
|
import { filterDefined } from 'common/util/array'
|
||||||
|
|
||||||
export const useGroup = (groupId: string | undefined) => {
|
export const useGroup = (groupId: string | undefined) => {
|
||||||
const [group, setGroup] = useState<Group | null | undefined>()
|
const [group, setGroup] = useState<Group | null | undefined>()
|
||||||
|
@ -29,11 +32,21 @@ export const useGroups = () => {
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMemberGroups = (userId: string | null | undefined) => {
|
export const useMemberGroups = (
|
||||||
|
userId: string | null | undefined,
|
||||||
|
options?: { withChatEnabled: boolean }
|
||||||
|
) => {
|
||||||
const [memberGroups, setMemberGroups] = useState<Group[] | undefined>()
|
const [memberGroups, setMemberGroups] = useState<Group[] | undefined>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) return listenForMemberGroups(userId, setMemberGroups)
|
if (userId)
|
||||||
}, [userId])
|
return listenForMemberGroups(userId, (groups) => {
|
||||||
|
if (options?.withChatEnabled)
|
||||||
|
return setMemberGroups(
|
||||||
|
filterDefined(groups.filter((group) => group.chatDisabled !== true))
|
||||||
|
)
|
||||||
|
return setMemberGroups(groups)
|
||||||
|
})
|
||||||
|
}, [options?.withChatEnabled, userId])
|
||||||
return memberGroups
|
return memberGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ export type Sort =
|
||||||
| 'newest'
|
| 'newest'
|
||||||
| 'oldest'
|
| 'oldest'
|
||||||
| 'most-traded'
|
| 'most-traded'
|
||||||
|
| 'most-popular'
|
||||||
| '24-hour-vol'
|
| '24-hour-vol'
|
||||||
| 'close-date'
|
| 'close-date'
|
||||||
| 'resolve-date'
|
| 'resolve-date'
|
||||||
|
@ -35,7 +36,7 @@ export function useInitialQueryAndSort(options?: {
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { defaultSort, shouldLoadFromStorage } = defaults(options, {
|
const { defaultSort, shouldLoadFromStorage } = defaults(options, {
|
||||||
defaultSort: '24-hour-vol',
|
defaultSort: 'most-popular',
|
||||||
shouldLoadFromStorage: true,
|
shouldLoadFromStorage: true,
|
||||||
})
|
})
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
|
@ -80,7 +80,3 @@ export function claimManalink(params: any) {
|
||||||
export function createGroup(params: any) {
|
export function createGroup(params: any) {
|
||||||
return call(getFunctionUrl('creategroup'), 'POST', params)
|
return call(getFunctionUrl('creategroup'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestBonuses(params: any) {
|
|
||||||
return call(getFunctionUrl('getdailybonuses'), 'POST', params)
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { sortBy, uniq } from 'lodash'
|
import { sortBy, uniq } from 'lodash'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { getContractFromId } from './contracts'
|
import { getContractFromId, updateContract } from './contracts'
|
||||||
import {
|
import {
|
||||||
coll,
|
coll,
|
||||||
getValue,
|
getValue,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
listenForValues,
|
listenForValues,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
|
||||||
export const groups = coll<Group>('groups')
|
export const groups = coll<Group>('groups')
|
||||||
|
|
||||||
|
@ -129,7 +130,22 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> {
|
||||||
return newGroup
|
return newGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addContractToGroup(group: Group, contractId: string) {
|
export async function addContractToGroup(group: Group, contract: Contract) {
|
||||||
|
await updateContract(contract.id, {
|
||||||
|
groupSlugs: [...(contract.groupSlugs ?? []), group.slug],
|
||||||
|
})
|
||||||
|
return await updateGroup(group, {
|
||||||
|
contractIds: uniq([...group.contractIds, contract.id]),
|
||||||
|
})
|
||||||
|
.then(() => group)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('error adding contract to group', err)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setContractGroupSlugs(group: Group, contractId: string) {
|
||||||
|
await updateContract(contractId, { groupSlugs: [group.slug] })
|
||||||
return await updateGroup(group, {
|
return await updateGroup(group, {
|
||||||
contractIds: uniq([...group.contractIds, contractId]),
|
contractIds: uniq([...group.contractIds, contractId]),
|
||||||
})
|
})
|
||||||
|
|
|
@ -61,6 +61,10 @@ export default function ContractSearchFirestore(props: {
|
||||||
)
|
)
|
||||||
} else if (sort === 'most-traded') {
|
} else if (sort === 'most-traded') {
|
||||||
matches.sort((a, b) => b.volume - a.volume)
|
matches.sort((a, b) => b.volume - a.volume)
|
||||||
|
} else if (sort === 'most-popular') {
|
||||||
|
matches.sort(
|
||||||
|
(a, b) => (b.uniqueBettorCount ?? 0) - (a.uniqueBettorCount ?? 0)
|
||||||
|
)
|
||||||
} else if (sort === '24-hour-vol') {
|
} else if (sort === '24-hour-vol') {
|
||||||
// Use lodash for stable sort, so previous sort breaks all ties.
|
// Use lodash for stable sort, so previous sort breaks all ties.
|
||||||
matches = sortBy(matches, ({ volume7Days }) => -1 * volume7Days)
|
matches = sortBy(matches, ({ volume7Days }) => -1 * volume7Days)
|
||||||
|
@ -107,6 +111,7 @@ export default function ContractSearchFirestore(props: {
|
||||||
>
|
>
|
||||||
<option value="newest">Newest</option>
|
<option value="newest">Newest</option>
|
||||||
<option value="oldest">Oldest</option>
|
<option value="oldest">Oldest</option>
|
||||||
|
<option value="most-popular">Most popular</option>
|
||||||
<option value="most-traded">Most traded</option>
|
<option value="most-traded">Most traded</option>
|
||||||
<option value="24-hour-vol">24h volume</option>
|
<option value="24-hour-vol">24h volume</option>
|
||||||
<option value="close-date">Closing soon</option>
|
<option value="close-date">Closing soon</option>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { addContractToGroup, getGroup } from 'web/lib/firebase/groups'
|
import { setContractGroupSlugs, getGroup } from 'web/lib/firebase/groups'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
|
||||||
|
@ -217,7 +217,7 @@ export function NewContract(props: {
|
||||||
isFree: false,
|
isFree: false,
|
||||||
})
|
})
|
||||||
if (result && selectedGroup) {
|
if (result && selectedGroup) {
|
||||||
await addContractToGroup(selectedGroup, result.id)
|
await setContractGroupSlugs(selectedGroup, result.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
await router.push(contractPath(result as Contract))
|
await router.push(contractPath(result as Contract))
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
getGroupBySlug,
|
getGroupBySlug,
|
||||||
getGroupContracts,
|
getGroupContracts,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
addContractToGroup,
|
|
||||||
addUserToGroup,
|
addUserToGroup,
|
||||||
|
addContractToGroup,
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
|
@ -54,6 +54,7 @@ import clsx from 'clsx'
|
||||||
import { FollowList } from 'web/components/follow-list'
|
import { FollowList } from 'web/components/follow-list'
|
||||||
import { SearchIcon } from '@heroicons/react/outline'
|
import { SearchIcon } from '@heroicons/react/outline'
|
||||||
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
import { useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
|
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -145,7 +146,7 @@ export default function GroupPage(props: {
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { slugs } = router.query as { slugs: string[] }
|
const { slugs } = router.query as { slugs: string[] }
|
||||||
const page = (slugs?.[1] ?? 'chat') as typeof groupSubpages[number]
|
const page = slugs?.[1] as typeof groupSubpages[number]
|
||||||
|
|
||||||
const group = useGroup(props.group?.id) ?? props.group
|
const group = useGroup(props.group?.id) ?? props.group
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>(undefined)
|
const [contracts, setContracts] = useState<Contract[] | undefined>(undefined)
|
||||||
|
@ -213,6 +214,75 @@ export default function GroupPage(props: {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
...(group.chatDisabled
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
title: 'Chat',
|
||||||
|
content: messages ? (
|
||||||
|
<GroupChat
|
||||||
|
messages={messages}
|
||||||
|
user={user}
|
||||||
|
group={group}
|
||||||
|
tips={tips}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LoadingIndicator />
|
||||||
|
),
|
||||||
|
href: groupPath(group.slug, 'chat'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
title: 'Questions',
|
||||||
|
content: (
|
||||||
|
<div className={'mt-2 px-1'}>
|
||||||
|
{contracts ? (
|
||||||
|
contracts.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => debouncedQuery(e.target.value)}
|
||||||
|
placeholder="Search the group's questions"
|
||||||
|
className="input input-bordered mb-4 w-full"
|
||||||
|
/>
|
||||||
|
<ContractsGrid
|
||||||
|
contracts={query != '' ? filteredContracts : contracts}
|
||||||
|
hasMore={false}
|
||||||
|
loadMore={() => {}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="p-2 text-gray-500">
|
||||||
|
No questions yet. Why not{' '}
|
||||||
|
<SiteLink
|
||||||
|
href={`/create/?groupId=${group.id}`}
|
||||||
|
className={'font-bold text-gray-700'}
|
||||||
|
>
|
||||||
|
add one?
|
||||||
|
</SiteLink>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<LoadingIndicator />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
href: groupPath(group.slug, 'questions'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rankings',
|
||||||
|
content: leaderboard,
|
||||||
|
href: groupPath(group.slug, 'rankings'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'About',
|
||||||
|
content: aboutTab,
|
||||||
|
href: groupPath(group.slug, 'about'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat')
|
||||||
return (
|
return (
|
||||||
<Page rightSidebar={rightSidebar} className="!pb-0">
|
<Page rightSidebar={rightSidebar} className="!pb-0">
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -250,79 +320,9 @@ export default function GroupPage(props: {
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
currentPageForAnalytics={groupPath(group.slug)}
|
currentPageForAnalytics={groupPath(group.slug)}
|
||||||
className={'mx-3 mb-0'}
|
className={'mb-0 sm:mb-2'}
|
||||||
defaultIndex={
|
defaultIndex={tabIndex > 0 ? tabIndex : 0}
|
||||||
page === 'rankings'
|
tabs={tabs}
|
||||||
? 2
|
|
||||||
: page === 'about'
|
|
||||||
? 3
|
|
||||||
: page === 'questions'
|
|
||||||
? 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
title: 'Chat',
|
|
||||||
content: messages ? (
|
|
||||||
<GroupChat
|
|
||||||
messages={messages}
|
|
||||||
user={user}
|
|
||||||
group={group}
|
|
||||||
tips={tips}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
),
|
|
||||||
href: groupPath(group.slug, 'chat'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Questions',
|
|
||||||
content: (
|
|
||||||
<div className={'mt-2 px-1'}>
|
|
||||||
{contracts ? (
|
|
||||||
contracts.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => debouncedQuery(e.target.value)}
|
|
||||||
placeholder="Search the group's questions"
|
|
||||||
className="input input-bordered mb-4 w-full"
|
|
||||||
/>
|
|
||||||
<ContractsGrid
|
|
||||||
contracts={query != '' ? filteredContracts : contracts}
|
|
||||||
hasMore={false}
|
|
||||||
loadMore={() => {}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="p-2 text-gray-500">
|
|
||||||
No questions yet. Why not{' '}
|
|
||||||
<SiteLink
|
|
||||||
href={`/create/?groupId=${group.id}`}
|
|
||||||
className={'font-bold text-gray-700'}
|
|
||||||
>
|
|
||||||
add one?
|
|
||||||
</SiteLink>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<LoadingIndicator />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
href: groupPath(group.slug, 'questions'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Rankings',
|
|
||||||
content: leaderboard,
|
|
||||||
href: groupPath(group.slug, 'rankings'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'About',
|
|
||||||
content: aboutTab,
|
|
||||||
href: groupPath(group.slug, 'about'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
@ -391,7 +391,16 @@ function GroupOverview(props: {
|
||||||
username={creator.username}
|
username={creator.username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
{isCreator ? (
|
||||||
|
<EditGroupButton className={'ml-1'} group={group} />
|
||||||
|
) : (
|
||||||
|
user &&
|
||||||
|
group.memberIds.includes(user?.id) && (
|
||||||
|
<Row>
|
||||||
|
<JoinOrLeaveGroupButton group={group} />
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<div className={'block sm:hidden'}>
|
<div className={'block sm:hidden'}>
|
||||||
<Linkify text={group.about} />
|
<Linkify text={group.about} />
|
||||||
|
@ -461,12 +470,19 @@ function GroupMemberSearch(props: { group: Group }) {
|
||||||
(m) =>
|
(m) =>
|
||||||
checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username)
|
checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username)
|
||||||
)
|
)
|
||||||
|
const matchLimit = 25
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SearchBar setQuery={setQuery} />
|
<SearchBar setQuery={setQuery} />
|
||||||
<Col className={'gap-2'}>
|
<Col className={'gap-2'}>
|
||||||
{matches.length > 0 && (
|
{matches.length > 0 && (
|
||||||
<FollowList userIds={matches.map((m) => m.id)} />
|
<FollowList userIds={matches.slice(0, matchLimit).map((m) => m.id)} />
|
||||||
|
)}
|
||||||
|
{matches.length > 25 && (
|
||||||
|
<div className={'text-center'}>
|
||||||
|
And {matches.length - matchLimit} more...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
|
@ -475,25 +491,21 @@ function GroupMemberSearch(props: { group: Group }) {
|
||||||
|
|
||||||
export function GroupMembersList(props: { group: Group }) {
|
export function GroupMembersList(props: { group: Group }) {
|
||||||
const { group } = props
|
const { group } = props
|
||||||
const members = useMembers(group)
|
const members = useMembers(group).filter((m) => m.id !== group.creatorId)
|
||||||
const maxMambersToShow = 5
|
const maxMembersToShow = 3
|
||||||
if (group.memberIds.length === 1) return <div />
|
if (group.memberIds.length === 1) return <div />
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="text-neutral flex flex-wrap gap-1">
|
||||||
<div>
|
<span className={'text-gray-500'}>Other members</span>
|
||||||
<div className="text-neutral flex flex-wrap gap-1">
|
{members.slice(0, maxMembersToShow).map((member, i) => (
|
||||||
<span className={'text-gray-500'}>Other members</span>
|
<div key={member.id} className={'flex-shrink'}>
|
||||||
{members.slice(0, maxMambersToShow).map((member, i) => (
|
<UserLink name={member.name} username={member.username} />
|
||||||
<div key={member.id} className={'flex-shrink'}>
|
{members.length > 1 && i !== members.length - 1 && <span>,</span>}
|
||||||
<UserLink name={member.name} username={member.username} />
|
|
||||||
{members.length > 1 && i !== members.length - 1 && <span>,</span>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{members.length > maxMambersToShow && (
|
|
||||||
<span> & {members.length - maxMambersToShow} more</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
{members.length > maxMembersToShow && (
|
||||||
|
<span> & {members.length - maxMembersToShow} more</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -503,8 +515,9 @@ function SortedLeaderboard(props: {
|
||||||
scoreFunction: (user: User) => number
|
scoreFunction: (user: User) => number
|
||||||
title: string
|
title: string
|
||||||
header: string
|
header: string
|
||||||
|
maxToShow?: number
|
||||||
}) {
|
}) {
|
||||||
const { users, scoreFunction, title, header } = props
|
const { users, scoreFunction, title, header, maxToShow } = props
|
||||||
const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a))
|
const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a))
|
||||||
return (
|
return (
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
|
@ -514,6 +527,7 @@ function SortedLeaderboard(props: {
|
||||||
columns={[
|
columns={[
|
||||||
{ header, renderCell: (user) => formatMoney(scoreFunction(user)) },
|
{ header, renderCell: (user) => formatMoney(scoreFunction(user)) },
|
||||||
]}
|
]}
|
||||||
|
maxToShow={maxToShow}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -528,7 +542,7 @@ function GroupLeaderboards(props: {
|
||||||
}) {
|
}) {
|
||||||
const { traderScores, creatorScores, members, topTraders, topCreators } =
|
const { traderScores, creatorScores, members, topTraders, topCreators } =
|
||||||
props
|
props
|
||||||
|
const maxToShow = 50
|
||||||
// Consider hiding M$0
|
// Consider hiding M$0
|
||||||
// If it's just one member (curator), show all bettors, otherwise just show members
|
// If it's just one member (curator), show all bettors, otherwise just show members
|
||||||
return (
|
return (
|
||||||
|
@ -541,12 +555,14 @@ function GroupLeaderboards(props: {
|
||||||
scoreFunction={(user) => traderScores[user.id] ?? 0}
|
scoreFunction={(user) => traderScores[user.id] ?? 0}
|
||||||
title="🏅 Bettor rankings"
|
title="🏅 Bettor rankings"
|
||||||
header="Profit"
|
header="Profit"
|
||||||
|
maxToShow={maxToShow}
|
||||||
/>
|
/>
|
||||||
<SortedLeaderboard
|
<SortedLeaderboard
|
||||||
users={members}
|
users={members}
|
||||||
scoreFunction={(user) => creatorScores[user.id] ?? 0}
|
scoreFunction={(user) => creatorScores[user.id] ?? 0}
|
||||||
title="🏅 Creator rankings"
|
title="🏅 Creator rankings"
|
||||||
header="Market volume"
|
header="Market volume"
|
||||||
|
maxToShow={maxToShow}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -561,6 +577,7 @@ function GroupLeaderboards(props: {
|
||||||
renderCell: (user) => formatMoney(traderScores[user.id] ?? 0),
|
renderCell: (user) => formatMoney(traderScores[user.id] ?? 0),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
maxToShow={maxToShow}
|
||||||
/>
|
/>
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
className="max-w-xl"
|
className="max-w-xl"
|
||||||
|
@ -573,6 +590,7 @@ function GroupLeaderboards(props: {
|
||||||
formatMoney(creatorScores[user.id] ?? 0),
|
formatMoney(creatorScores[user.id] ?? 0),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
maxToShow={maxToShow}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -586,7 +604,7 @@ function AddContractButton(props: { group: Group; user: User }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
async function addContractToCurrentGroup(contract: Contract) {
|
async function addContractToCurrentGroup(contract: Contract) {
|
||||||
await addContractToGroup(group, contract.id)
|
await addContractToGroup(group, contract)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Col } from 'web/components/layout/col'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { UserLink } from 'web/components/user-page'
|
|
||||||
import { useGroups, useMemberGroupIds } from 'web/hooks/use-group'
|
import { useGroups, useMemberGroupIds } from 'web/hooks/use-group'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { groupPath, listAllGroups } from 'web/lib/firebase/groups'
|
import { groupPath, listAllGroups } from 'web/lib/firebase/groups'
|
||||||
|
@ -17,6 +16,8 @@ import { GroupMembersList } from 'web/pages/group/[...slugs]'
|
||||||
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
|
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const groups = await listAllGroups().catch((_) => [])
|
const groups = await listAllGroups().catch((_) => [])
|
||||||
|
@ -92,79 +93,75 @@ export default function Groups(props: {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-xl">
|
<Col className="w-full max-w-2xl px-4 sm:px-2">
|
||||||
<Col className="px-4 sm:px-0">
|
<Row className="items-center justify-between">
|
||||||
<Row className="items-center justify-between">
|
<Title text="Explore groups" />
|
||||||
<Title text="Explore groups" />
|
{user && <CreateGroupButton user={user} goToGroupOnSubmit={true} />}
|
||||||
{user && (
|
</Row>
|
||||||
<CreateGroupButton user={user} goToGroupOnSubmit={true} />
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<div className="mb-6 text-gray-500">
|
<div className="mb-6 text-gray-500">
|
||||||
Discuss and compete on questions with a group of friends.
|
Discuss and compete on questions with a group of friends.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
currentPageForAnalytics={'groups'}
|
currentPageForAnalytics={'groups'}
|
||||||
tabs={[
|
tabs={[
|
||||||
...(user && memberGroupIds.length > 0
|
...(user && memberGroupIds.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: 'My Groups',
|
title: 'My Groups',
|
||||||
content: (
|
content: (
|
||||||
<Col>
|
<Col>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onChange={(e) => debouncedQuery(e.target.value)}
|
onChange={(e) => debouncedQuery(e.target.value)}
|
||||||
placeholder="Search your groups"
|
placeholder="Search your groups"
|
||||||
className="input input-bordered mb-4 w-full"
|
className="input input-bordered mb-4 w-full"
|
||||||
/>
|
|
||||||
|
|
||||||
<Col className="gap-4">
|
|
||||||
{matchesOrderedByRecentActivity
|
|
||||||
.filter((match) =>
|
|
||||||
memberGroupIds.includes(match.id)
|
|
||||||
)
|
|
||||||
.map((group) => (
|
|
||||||
<GroupCard
|
|
||||||
key={group.id}
|
|
||||||
group={group}
|
|
||||||
creator={creatorsDict[group.creatorId]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Col>
|
|
||||||
</Col>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: 'All',
|
|
||||||
content: (
|
|
||||||
<Col>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onChange={(e) => debouncedQuery(e.target.value)}
|
|
||||||
placeholder="Search groups"
|
|
||||||
className="input input-bordered mb-4 w-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Col className="gap-4">
|
|
||||||
{matches.map((group) => (
|
|
||||||
<GroupCard
|
|
||||||
key={group.id}
|
|
||||||
group={group}
|
|
||||||
creator={creatorsDict[group.creatorId]}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</Col>
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
</Col>
|
{matchesOrderedByRecentActivity
|
||||||
),
|
.filter((match) =>
|
||||||
},
|
memberGroupIds.includes(match.id)
|
||||||
]}
|
)
|
||||||
/>
|
.map((group) => (
|
||||||
</Col>
|
<GroupCard
|
||||||
|
key={group.id}
|
||||||
|
group={group}
|
||||||
|
creator={creatorsDict[group.creatorId]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: 'All',
|
||||||
|
content: (
|
||||||
|
<Col>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => debouncedQuery(e.target.value)}
|
||||||
|
placeholder="Search groups"
|
||||||
|
className="input input-bordered mb-4 w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
{matches.map((group) => (
|
||||||
|
<GroupCard
|
||||||
|
key={group.id}
|
||||||
|
group={group}
|
||||||
|
creator={creatorsDict[group.creatorId]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -176,32 +173,33 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) {
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
key={group.id}
|
key={group.id}
|
||||||
className="relative gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100"
|
className="relative min-w-[20rem] max-w-xs gap-1 rounded-xl bg-white p-8 shadow-md hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<Link href={groupPath(group.slug)}>
|
<Link href={groupPath(group.slug)}>
|
||||||
<a className="absolute left-0 right-0 top-0 bottom-0" />
|
<a className="absolute left-0 right-0 top-0 bottom-0 z-0" />
|
||||||
</Link>
|
</Link>
|
||||||
|
<div>
|
||||||
|
<Avatar
|
||||||
|
className={'absolute top-2 right-2'}
|
||||||
|
username={creator?.username}
|
||||||
|
avatarUrl={creator?.avatarUrl}
|
||||||
|
noLink={false}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Row className="items-center justify-between gap-2">
|
<Row className="items-center justify-between gap-2">
|
||||||
<span className="text-xl">{group.name}</span>
|
<span className="text-xl">{group.name}</span>
|
||||||
</Row>
|
</Row>
|
||||||
<div className="flex flex-col items-start justify-start gap-2 text-sm text-gray-500 ">
|
<Row>{group.contractIds.length} questions</Row>
|
||||||
<Row>
|
<Row className="text-sm text-gray-500">
|
||||||
{group.contractIds.length} questions
|
<GroupMembersList group={group} />
|
||||||
<div className={'mx-2'}>•</div>
|
</Row>
|
||||||
<div className="mr-1">Created by</div>
|
<Row>
|
||||||
<UserLink
|
<div className="text-sm text-gray-500">{group.about}</div>
|
||||||
className="text-neutral"
|
</Row>
|
||||||
name={creator?.name ?? ''}
|
<Col className={'mt-2 h-full items-start justify-end'}>
|
||||||
username={creator?.username ?? ''}
|
<JoinOrLeaveGroupButton group={group} className={'z-10 w-24'} />
|
||||||
/>
|
</Col>
|
||||||
</Row>
|
|
||||||
{group.memberIds.length > 1 && (
|
|
||||||
<Row>
|
|
||||||
<GroupMembersList group={group} />
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">{group.about}</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ const Home = () => {
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
querySortOptions={{
|
querySortOptions={{
|
||||||
shouldLoadFromStorage: true,
|
shouldLoadFromStorage: true,
|
||||||
defaultSort: getSavedSort() ?? '24-hour-vol',
|
defaultSort: getSavedSort() ?? 'most-popular',
|
||||||
}}
|
}}
|
||||||
onContractClick={(c) => {
|
onContractClick={(c) => {
|
||||||
// Show contract without navigating to contract page.
|
// Show contract without navigating to contract page.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user