Files
modrinth/packages/ui/src/components/user/UserBadges.vue
T
2026-05-31 16:22:16 -07:00

508 lines
11 KiB
Vue

<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
AlphaBadge,
BetaBadge,
Downloads1mBadge,
Downloads10mBadge,
Downloads25mBadge,
Downloads50mBadge,
Downloads100mBadge,
Downloads250mBadge,
Downloads500mBadge,
EarlyDatapackBadge,
EarlyHostingBadge,
EarlyModpackBadge,
EarlyPluginBadge,
EarlyResourcepackBadge,
EarlyServersBadge,
EarlyShadersBadge,
ModeratorBadge,
PlusBadge,
PrideBadge,
StaffBadge,
} from '@modrinth/assets'
import {
defineMessage,
defineMessages,
type MessageDescriptor,
useFormatNumber,
useVIntl,
} from '@modrinth/ui'
import { UserBadge as BadgeBitflag } from '@modrinth/utils'
import { type Component, computed } from 'vue'
import UserBadge from './UserBadge.vue'
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
type EarlyAdopterProjectTypes =
| 'modpack'
| 'resourcepack'
| 'plugin'
| 'datapack'
| 'shader'
| 'server'
type BadgeCriterion =
| {
type: 'earliest_project_date'
project_type: EarlyAdopterProjectTypes
cutoff: Date
}
| {
type: 'join_date'
cutoff: Date
}
| {
type: 'badge'
bitflag: number
}
| {
type: 'midas'
}
| {
type: 'pride'
}
| {
type: 'role'
role: Labrinth.Users.v3.Role
}
type Badge = {
icon: Component
name: MessageDescriptor
about: MessageDescriptor[]
criteria: BadgeCriterion[] // if any criterion matches, the badge will apply (OR logic)
link?: {
href: string
message: MessageDescriptor
}
}
const BADGES = [
{
icon: StaffBadge,
name: defineMessage({
id: 'user.profile.badge.staff.name',
defaultMessage: 'Modrinth Team',
}),
about: [
defineMessage({
id: 'user.profile.badge.staff.about.1',
defaultMessage: `This user works for Modrinth.`,
}),
],
criteria: [
{
type: 'role',
role: 'admin',
},
{
type: 'role',
role: 'moderator',
},
],
},
{
icon: ModeratorBadge,
name: defineMessage({
id: 'user.profile.badge.moderator.name',
defaultMessage: 'Content Moderator',
}),
about: [
defineMessage({
id: 'user.profile.badge.moderator.about.1',
defaultMessage: `This user works for Modrinth as a Content Moderator.`,
}),
defineMessage({
id: 'user.profile.badge.moderator.about.2',
defaultMessage: `Content Moderators on Modrinth review projects, handle reports, and help keep Modrinth safe.`,
}),
],
criteria: [
{
type: 'role',
role: 'moderator',
},
],
},
{
icon: AlphaBadge,
name: defineMessage({
id: 'user.profile.badge.alpha.name',
defaultMessage: 'Alpha Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.alpha.about.1',
defaultMessage: `This user has been around since Modrinth Alpha, which ended in November 2020.`,
}),
],
criteria: [
{
type: 'badge',
bitflag: BadgeBitflag.ALPHA_TESTER,
},
{
type: 'join_date',
cutoff: new Date('2020-11-30T08:00:00.000Z'),
},
],
},
{
icon: BetaBadge,
name: defineMessage({
id: 'user.profile.badge.beta.name',
defaultMessage: 'Beta Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.beta.about.1',
defaultMessage: `This user has been around since Modrinth Beta, which ended in February 2022.`,
}),
],
criteria: [
{
type: 'join_date',
cutoff: new Date('2022-02-27T08:00:00.000Z'),
},
],
link: {
href: 'https://modrinth.com/news/article/modrinth-beta/',
message: defineMessage({
id: 'user.profile.badge.beta.link',
defaultMessage: `Click to read about the launch of Modrinth Beta.`,
}),
},
},
{
icon: PlusBadge,
name: defineMessage({
id: 'user.profile.badge.plus.name',
defaultMessage: 'Modrinth+ Member',
}),
about: [
defineMessage({
id: 'user.profile.badge.plus.about.1',
defaultMessage: `This user is going the extra mile to support Modrinth and the creators on the platform.`,
}),
],
criteria: [
{
type: 'badge',
bitflag: BadgeBitflag.MIDAS,
},
{
type: 'midas',
},
],
link: {
href: 'https://modrinth.com/plus',
message: defineMessage({
id: 'user.profile.badge.plus.link',
defaultMessage: `Click to learn more about how you can become a member.`,
}),
},
},
{
icon: PrideBadge,
name: defineMessage({
id: 'user.profile.badge.pride.name',
defaultMessage: 'Pride Fundraiser Supporter',
}),
about: [
defineMessage({
id: 'user.profile.badge.pride.about.1',
defaultMessage: `This user participated in at least one of Modrinth's Pride fundraisers for the LGBTQ+ community.`,
}),
],
criteria: [
{
type: 'pride',
},
],
link: {
href: 'https://modrinth.com/pride?from=badge',
message: defineMessage({
id: 'user.profile.badge.pride.link',
defaultMessage: `Click to visit our latest Pride fundraiser.`,
}),
},
},
{
icon: EarlyModpackBadge,
name: defineMessage({
id: 'user.profile.badge.early-modpack-adopter.name',
defaultMessage: 'Early Modpack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-modpack-adopter.about.1',
defaultMessage: `This user helped us test Modpack projects on Modrinth before we launched them in May 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'modpack',
cutoff: new Date('2022-05-23T00:57:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_MODPACK_ADOPTER,
},
],
},
{
icon: EarlyResourcepackBadge,
name: defineMessage({
id: 'user.profile.badge.early-resourcepack-adopter.name',
defaultMessage: 'Early Resource Pack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-resourcepack-adopter.about.1',
defaultMessage: `This user helped us test Resource Pack projects on Modrinth before we launched them in August 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'resourcepack',
cutoff: new Date('2022-08-27T23:03:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_RESPACK_ADOPTER,
},
],
},
{
icon: EarlyPluginBadge,
name: defineMessage({
id: 'user.profile.badge.early-plugin-adopter.name',
defaultMessage: 'Early Plugin Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-plugin-adopter.about.1',
defaultMessage: `This user helped us test Plugin projects on Modrinth before we launched them in August 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'plugin',
cutoff: new Date('2022-08-27T23:03:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_PLUGIN_ADOPTER,
},
],
},
{
icon: EarlyDatapackBadge,
name: defineMessage({
id: 'user.profile.badge.early-datapack-adopter.name',
defaultMessage: 'Early Data Pack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-datapack-adopter.about.1',
defaultMessage: `This user helped us test Data Pack projects on Modrinth before we launched them in January 2023.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'datapack',
cutoff: new Date('2023-01-08T02:00:00.000Z'),
},
],
},
{
icon: EarlyShadersBadge,
name: defineMessage({
id: 'user.profile.badge.early-shader-adopter.name',
defaultMessage: 'Early Shader Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-shader-adopter.about.1',
defaultMessage: `This user helped us test Shader projects on Modrinth before we launched them in January 2023.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'shader',
cutoff: new Date('2023-01-08T02:00:00.000Z'),
},
],
},
{
icon: EarlyServersBadge,
name: defineMessage({
id: 'user.profile.badge.early-server-adopter.name',
defaultMessage: 'Early Server Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-server-adopter.about.1',
defaultMessage: `This user helped us test Server projects on Modrinth before we launched them in March 2026.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'server',
cutoff: new Date('2026-03-04T01:33:00.000Z'),
},
],
},
{
icon: EarlyHostingBadge,
name: defineMessage({
id: 'user.profile.badge.hosting-alpha.name',
defaultMessage: 'Modrinth Hosting Alpha Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.hosting-alpha.about.1',
defaultMessage: `This user participated in a closed alpha test of Modrinth Hosting before we launched Modrinth Hosting Beta in November 2024.`,
}),
],
criteria: [], // TODO: Add badge on backend for Hosting Alpha Tester
},
] satisfies Badge[]
const DOWNLOAD_BADGES = [
{
icon: Downloads1mBadge,
threshold: 1_000_000,
},
{
icon: Downloads10mBadge,
threshold: 10_000_000,
},
{
icon: Downloads25mBadge,
threshold: 25_000_000,
},
{
icon: Downloads50mBadge,
threshold: 50_000_000,
},
{
icon: Downloads100mBadge,
threshold: 100_000_000,
},
{
icon: Downloads250mBadge,
threshold: 250_000_000,
},
{
icon: Downloads500mBadge,
threshold: 500_000_000,
},
].sort((a, b) => b.threshold - a.threshold)
const props = defineProps<{
role: Labrinth.Users.v2.Role
badges: number
hasMidas?: boolean
hasPride?: boolean
downloads: number
joinDate: Date
earliestProjectByType: Record<EarlyAdopterProjectTypes, Date>
}>()
const downloadsBadge = computed(() => {
return DOWNLOAD_BADGES.find((badge) => props.downloads >= badge.threshold)
})
const messages = defineMessages({
title: {
id: 'profile.label.badges',
defaultMessage: 'Badges',
},
downloadsBadgeName: {
id: 'user.profile.badge.downloads.name',
defaultMessage: '{download_sum} Downloads',
},
downloadsBadgeAbout1: {
id: 'user.profile.badge.downloads.about.1',
defaultMessage: `This user's projects have collectively achieved {download_sum} downloads.`,
},
})
function passesCriterion(criterion: BadgeCriterion) {
switch (criterion.type) {
case 'role': {
return props.role === criterion.role
}
case 'badge': {
return props.badges & criterion.bitflag
}
case 'midas': {
return props.hasMidas === true
}
case 'pride': {
return props.hasPride === true
}
case 'join_date': {
return props.joinDate < criterion.cutoff
}
case 'earliest_project_date': {
const date = props.earliestProjectByType[criterion.project_type]
return date && date < criterion.cutoff
}
default: {
return false
}
}
}
const earnedBadges = computed(() => {
const badges: Badge[] = []
loopingBadges: for (const badge of BADGES) {
for (const criterion of badge.criteria) {
if (passesCriterion(criterion)) {
badges.push(badge)
continue loopingBadges
}
}
}
return badges
})
</script>
<template>
<div v-if="earnedBadges.length > 0 || !!downloadsBadge" class="flex flex-col">
<h2 class="text-lg text-contrast m-0 mb-2">
{{ formatMessage(messages.title) }}
</h2>
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-2">
<UserBadge
v-for="badge in earnedBadges"
:key="badge.name.id"
:name="badge.name"
:icon="badge.icon"
:about="badge.about"
:link="badge.link"
/>
<UserBadge
v-if="downloadsBadge"
:name="messages.downloadsBadgeName"
:icon="downloadsBadge.icon"
:about="[messages.downloadsBadgeAbout1]"
:values="{ download_sum: formatNumber(downloadsBadge.threshold) }"
/>
</div>
</div>
</template>