Add badges by rarities to profile

This commit is contained in:
Ian Philips 2022-10-10 13:56:22 -06:00
parent d6e7ecfe1f
commit f24600755b
9 changed files with 201 additions and 57 deletions

View File

@ -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'

View File

@ -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[]
}
}

View File

@ -332,7 +332,6 @@ async function handleBettingStreakBadgeAward(
achievements: {
...user.achievements,
streaker: {
totalBadges: (user.achievements?.streaker?.totalBadges ?? 0) + 1,
badges: [...(user.achievements?.streaker?.badges ?? []), badge],
},
},

View File

@ -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,

View File

@ -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,

View File

@ -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<Contract>(
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
}

View File

@ -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

View File

@ -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 }) {
<Col className={'cursor-default text-center'}>
<Medal rarity={rarity} />
<Tooltip
text={`Make predictions ${totalBettingStreak} day
${totalBettingStreak > 1 ? 's' : ''} in a row`}
text={`Make ${PAST_BETS} ${totalBettingStreak} day${
totalBettingStreak > 1 ? 's' : ''
} in a row`}
>
<span
className={
@ -195,7 +197,7 @@ function MarketCreatorBadgeItem(props: {
: bronzeClassName
}
>
Market Maker
Market Creator
</span>
</Tooltip>
</Col>

View File

@ -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 }) {
<span className="break-anywhere text-lg font-bold sm:text-2xl">
{user.name}
</span>
<span className="sm:text-md text-greyscale-4 text-sm">
@{user.username}
</span>
<Row className="sm:text-md -mt-1 items-center gap-x-3 text-sm ">
<span className={' text-greyscale-4'}>@{user.username}</span>
<BadgeDisplay user={user} router={router} />
</Row>
</Col>
{isCurrentUser && (
<ProfilePrivateStats
@ -280,11 +286,8 @@ export function ProfilePrivateStats(props: {
}) {
const { profit, user, router } = props
const [showLoansModal, setShowLoansModal] = useState(false)
const [showBadgesModal, setShowBadgesModal] = useState(false)
useEffect(() => {
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: {
</span>
<span className="mx-auto text-xs sm:text-sm">profit</span>
</Col>
<Col
className={'text-md flex-shrink-0 cursor-pointer sm:text-lg'}
onClick={() => setShowBadgesModal(true)}
>
<span>🏅 {calculateTotalUsersBadges(user)}</span>
<span className={'text-greyscale-4 mx-auto text-xs sm:text-sm'}>
badges
</span>
</Col>
<Col
className={
'text-greyscale-4 text-md flex-shrink-0 cursor-pointer sm:text-lg'
@ -321,11 +315,6 @@ export function ProfilePrivateStats(props: {
<span className="mx-auto text-xs sm:text-sm">next loan</span>
</Col>
</Row>
<BadgesModal
isOpen={showBadgesModal}
setOpen={setShowBadgesModal}
user={user}
/>
{showLoansModal && (
<LoansModal isOpen={showLoansModal} setOpen={setShowLoansModal} />
)}
@ -345,3 +334,49 @@ export function ProfilePublicStats(props: { user: User; className?: string }) {
</Row>
)
}
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 (
<Row
key={rarity}
className={clsx(
'items-center gap-2',
rarity === 'bronze'
? bronzeClassName
: rarity === 'silver'
? silverClassName
: goldClassName
)}
>
<span className={clsx('-m-0.5 text-lg')}></span>
<span className="text-xs">{numBadges}</span>
</Row>
)
}
)
return (
<Row
className={'cursor-pointer gap-2'}
onClick={() => setShowBadgesModal(true)}
>
{badgesByRarityItems}
<BadgesModal
isOpen={showBadgesModal}
setOpen={setShowBadgesModal}
user={user}
/>
</Row>
)
}