From f24600755ba9be1e7a6e702db84d913f36b7a41a Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 10 Oct 2022 13:56:22 -0600 Subject: [PATCH] Add badges by rarities to profile --- common/badge.ts | 40 +++++---- common/user.ts | 5 +- functions/src/on-create-bet.ts | 1 - functions/src/on-create-contract.ts | 9 +- functions/src/on-update-contract.ts | 2 - functions/src/scripts/backfill-badges.ts | 102 +++++++++++++++++++++++ functions/src/utils.ts | 6 ++ web/components/profile/badges-modal.tsx | 16 ++-- web/components/user-page.tsx | 77 ++++++++++++----- 9 files changed, 201 insertions(+), 57 deletions(-) create mode 100644 functions/src/scripts/backfill-badges.ts diff --git a/common/badge.ts b/common/badge.ts index 0efefc14..61d98e4a 100644 --- a/common/badge.ts +++ b/common/badge.ts @@ -4,7 +4,7 @@ export type Badge = { type: BadgeTypes createdTime: number data: { [key: string]: any } - name: 'Proven Correct' | 'Streaker' | 'Market Maker' + name: 'Proven Correct' | 'Streaker' | 'Market Creator' } export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR' @@ -53,7 +53,7 @@ const calculateProvenCorrectBadgeRarity = (badge: ProvenCorrectBadge) => { return 1 } -export const streakerBadgeRarityThresholds = [1, 50, 200] +export const streakerBadgeRarityThresholds = [1, 50, 250] const calculateStreakerBadgeRarity = (badge: StreakerBadge) => { const { totalBettingStreak } = badge.data const thresholdArray = streakerBadgeRarityThresholds @@ -67,10 +67,10 @@ const calculateStreakerBadgeRarity = (badge: StreakerBadge) => { return 1 } -export const marketMakerBadgeRarityThresholds = [1, 50, 200] -const calculateMarketMakerBadgeRarity = (badge: MarketCreatorBadge) => { +export const marketCreatorBadgeRarityThresholds = [1, 75, 300] +const calculateMarketCreatorBadgeRarity = (badge: MarketCreatorBadge) => { const { totalContractsCreated } = badge.data - const thresholdArray = marketMakerBadgeRarityThresholds + const thresholdArray = marketCreatorBadgeRarityThresholds let i = thresholdArray.length - 1 while (i >= 0) { if (totalContractsCreated == thresholdArray[i]) { @@ -81,10 +81,9 @@ const calculateMarketMakerBadgeRarity = (badge: MarketCreatorBadge) => { return 1 } -export type rarities = 'common' | 'bronze' | 'silver' | 'gold' +export type rarities = 'bronze' | 'silver' | 'gold' const rarityRanks: { [key: number]: rarities } = { - 0: 'common', 1: 'bronze', 2: 'silver', 3: 'gold', @@ -98,7 +97,7 @@ export const calculateBadgeRarity = (badge: Badge) => { ] case 'MARKET_CREATOR': return rarityRanks[ - calculateMarketMakerBadgeRarity(badge as MarketCreatorBadge) + calculateMarketCreatorBadgeRarity(badge as MarketCreatorBadge) ] case 'STREAKER': return rarityRanks[calculateStreakerBadgeRarity(badge as StreakerBadge)] @@ -107,12 +106,21 @@ export const calculateBadgeRarity = (badge: Badge) => { } } -export const calculateTotalUsersBadges = (user: User) => { - const { achievements } = user - if (!achievements) return 0 - return ( - (achievements.marketCreator?.totalBadges ?? 0) + - (achievements.provenCorrect?.totalBadges ?? 0) + - (achievements.streaker?.totalBadges ?? 0) - ) +export const getBadgesByRarity = (user: User) => { + const rarities: { [key in rarities]: number } = { + bronze: 0, + silver: 0, + gold: 0, + } + Object.values(user.achievements).map((value) => { + value.badges.map((badge) => { + rarities[calculateBadgeRarity(badge)] = + (rarities[calculateBadgeRarity(badge)] ?? 0) + 1 + }) + }) + return rarities } + +export const goldClassName = 'text-amber-400' +export const silverClassName = 'text-gray-500' +export const bronzeClassName = 'text-amber-900' diff --git a/common/user.ts b/common/user.ts index 3bce63c7..f00dfc89 100644 --- a/common/user.ts +++ b/common/user.ts @@ -53,17 +53,14 @@ export type User = { freeMarketsCreated?: number isBannedFromPosting?: boolean - achievements?: { + achievements: { provenCorrect?: { - totalBadges: number badges: ProvenCorrectBadge[] } marketCreator?: { - totalBadges: number badges: MarketCreatorBadge[] } streaker?: { - totalBadges: number badges: StreakerBadge[] } } diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index 3de3a101..7496db03 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -332,7 +332,6 @@ async function handleBettingStreakBadgeAward( achievements: { ...user.achievements, streaker: { - totalBadges: (user.achievements?.streaker?.totalBadges ?? 0) + 1, badges: [...(user.achievements?.streaker?.badges ?? []), badge], }, }, diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index d16534fe..13a84575 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -13,7 +13,7 @@ import { User } from '../../common/user' import * as admin from 'firebase-admin' import { MarketCreatorBadge, - marketMakerBadgeRarityThresholds, + marketCreatorBadgeRarityThresholds, } from '../../common/badge' export const onCreateContract = functions @@ -50,10 +50,10 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) { .where('creatorId', '==', contractCreator.id) .where('resolution', '!=', 'CANCEL') ) - if (contracts.length in marketMakerBadgeRarityThresholds) { + if (contracts.length in marketCreatorBadgeRarityThresholds) { const badge = { type: 'MARKET_CREATOR', - name: 'Market Maker', + name: 'Market Creator', data: { totalContractsCreated: contracts.length, }, @@ -67,9 +67,6 @@ async function handleMarketCreatorBadgeAward(contractCreator: User) { achievements: { ...contractCreator.achievements, marketCreator: { - totalBadges: - (contractCreator.achievements?.marketCreator?.totalBadges ?? 0) + - 1, badges: [ ...(contractCreator.achievements?.marketCreator?.badges ?? []), badge, diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index ccb1c941..f0aa0252 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -82,8 +82,6 @@ async function handleResolvedContract(contract: Contract) { achievements: { ...user.achievements, provenCorrect: { - totalBadges: - (user.achievements?.provenCorrect?.totalBadges ?? 0) + 1, badges: [ ...(user.achievements?.provenCorrect?.badges ?? []), newProvenCorrectBadge, diff --git a/functions/src/scripts/backfill-badges.ts b/functions/src/scripts/backfill-badges.ts new file mode 100644 index 00000000..5218ed7c --- /dev/null +++ b/functions/src/scripts/backfill-badges.ts @@ -0,0 +1,102 @@ +import * as admin from 'firebase-admin' + +import { initAdmin } from './script-init' +import { getUser, getValues } from '../utils' +import { Contract } from 'common/contract' +import { + MarketCreatorBadge, + marketCreatorBadgeRarityThresholds, + StreakerBadge, + streakerBadgeRarityThresholds, +} from 'common/badge' +import { User } from 'common/user' +import { filterDefined } from 'common/util/array' +initAdmin() + +const firestore = admin.firestore() + +async function main() { + // const users = await getAllUsers() + // const users = filterDefined([await getUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')]) + const users = filterDefined([await getUser('AJwLWoo3xue32XIiAVrL5SyR1WB2')]) + await Promise.all( + users.map(async (user) => { + console.log('Added achievements to user', user.id) + if (!user.id) return + if (user.achievements === undefined) { + await firestore.collection('users').doc(user.id).update({ + achievements: {}, + }) + user.achievements = {} + } + user.achievements = await awardMarketCreatorBadges(user) + user.achievements = await awardBettingStreakBadges(user) + // going to ignore backfilling the proven correct badges for now + }) + ) +} + +if (require.main === module) main().then(() => process.exit()) + +async function awardMarketCreatorBadges(user: User) { + // Award market maker badges + const contracts = await getValues( + firestore + .collection(`contracts`) + .where('creatorId', '==', user.id) + .where('resolution', '!=', 'CANCEL') + ) + + const achievements = { + ...user.achievements, + marketCreator: { + badges: [...(user.achievements.marketCreator?.badges ?? [])], + }, + } + for (const threshold of marketCreatorBadgeRarityThresholds) { + if (contracts.length >= threshold) { + const badge = { + type: 'MARKET_CREATOR', + name: 'Market Creator', + data: { + totalContractsCreated: threshold, + }, + createdTime: Date.now(), + } as MarketCreatorBadge + achievements.marketCreator.badges.push(badge) + } + } + // update user + await firestore.collection('users').doc(user.id).update({ + achievements, + }) + return achievements +} + +async function awardBettingStreakBadges(user: User) { + const streak = user.currentBettingStreak ?? 0 + const achievements = { + ...user.achievements, + streaker: { + badges: [...(user.achievements?.streaker?.badges ?? [])], + }, + } + for (const threshold of streakerBadgeRarityThresholds) { + if (streak >= threshold) { + const badge = { + type: 'STREAKER', + name: 'Streaker', + data: { + totalBettingStreak: threshold, + }, + createdTime: Date.now(), + } as StreakerBadge + achievements.streaker.badges.push(badge) + } + } + // update user + await firestore.collection('users').doc(user.id).update({ + achievements, + }) + return achievements +} diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 91f4b293..e0cd269a 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -112,6 +112,12 @@ export const getAllPrivateUsers = async () => { return users.docs.map((doc) => doc.data() as PrivateUser) } +export const getAllUsers = async () => { + const firestore = admin.firestore() + const users = await firestore.collection('users').get() + return users.docs.map((doc) => doc.data() as User) +} + export const getUserByUsername = async (username: string) => { const firestore = admin.firestore() const snap = await firestore diff --git a/web/components/profile/badges-modal.tsx b/web/components/profile/badges-modal.tsx index 8fe5ae72..68338abd 100644 --- a/web/components/profile/badges-modal.tsx +++ b/web/components/profile/badges-modal.tsx @@ -1,13 +1,16 @@ import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' -import { User } from 'common/user' +import { PAST_BETS, User } from 'common/user' import clsx from 'clsx' import { Badge, + bronzeClassName, calculateBadgeRarity, + goldClassName, MarketCreatorBadge, ProvenCorrectBadge, rarities, + silverClassName, StreakerBadge, } from 'common/badge' import { groupBy } from 'lodash' @@ -15,9 +18,7 @@ import { Row } from 'web/components/layout/row' import { SiteLink } from 'web/components/site-link' import { contractPathWithoutContract } from 'web/lib/firebase/contracts' import { Tooltip } from 'web/components/tooltip' -const goldClassName = 'text-amber-400' -const silverClassName = 'text-gray-500' -const bronzeClassName = 'text-amber-900' + export function BadgesModal(props: { isOpen: boolean setOpen: (open: boolean) => void @@ -154,8 +155,9 @@ function StreakerBadgeItem(props: { badge: StreakerBadge; rarity: rarities }) { 1 ? 's' : ''} in a row`} + text={`Make ${PAST_BETS} ${totalBettingStreak} day${ + totalBettingStreak > 1 ? 's' : '' + } in a row`} > - Market Maker + Market Creator diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 00326035..c510807a 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -37,7 +37,12 @@ import { BadgesModal } from 'web/components/profile/badges-modal' import { copyToClipboard } from 'web/lib/util/copy' import { track } from 'web/lib/service/analytics' import { DOMAIN } from 'common/envs/constants' -import { calculateTotalUsersBadges } from 'common/badge' +import { + bronzeClassName, + getBadgesByRarity, + goldClassName, + silverClassName, +} from 'common/badge' export function UserPage(props: { user: User }) { const { user } = props @@ -101,9 +106,10 @@ export function UserPage(props: { user: User }) { {user.name} - - @{user.username} - + + @{user.username} + + {isCurrentUser && ( { - const showBadgesModal = router.query['show'] === 'badges' - setShowBadgesModal(showBadgesModal) const showLoansModel = router.query['show'] === 'loans' setShowLoansModal(showLoansModel) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -300,15 +303,6 @@ export function ProfilePrivateStats(props: { profit - setShowBadgesModal(true)} - > - 🏅 {calculateTotalUsersBadges(user)} - - badges - - next loan - {showLoansModal && ( )} @@ -345,3 +334,49 @@ export function ProfilePublicStats(props: { user: User; className?: string }) { ) } + +function BadgeDisplay(props: { user: User; router: NextRouter }) { + const { user, router } = props + const [showBadgesModal, setShowBadgesModal] = useState(false) + + useEffect(() => { + const showBadgesModal = router.query['show'] === 'badges' + setShowBadgesModal(showBadgesModal) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + // get number of badges of each rarity type + const badgesByRarity = getBadgesByRarity(user) + const badgesByRarityItems = Object.entries(badgesByRarity).map( + ([rarity, numBadges]) => { + return ( + + + {numBadges} + + ) + } + ) + return ( + setShowBadgesModal(true)} + > + {badgesByRarityItems} + + + ) +}