Add badges by rarities to profile
This commit is contained in:
parent
d6e7ecfe1f
commit
f24600755b
|
@ -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'
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,7 +332,6 @@ async function handleBettingStreakBadgeAward(
|
|||
achievements: {
|
||||
...user.achievements,
|
||||
streaker: {
|
||||
totalBadges: (user.achievements?.streaker?.totalBadges ?? 0) + 1,
|
||||
badges: [...(user.achievements?.streaker?.badges ?? []), badge],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
102
functions/src/scripts/backfill-badges.ts
Normal file
102
functions/src/scripts/backfill-badges.ts
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user