feat: new user badges, ui consistency pass (#6262)

* feat: new user badges, ui consistency pass

* prepr

* fix: align with backend

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Prospector
2026-05-31 15:25:31 +00:00
committed by GitHub
co-authored by Calum H.
parent cc8d556448
commit 34b87991bc
47 changed files with 947 additions and 142 deletions
+2 -6
View File
@@ -38,11 +38,7 @@
{{ calculateSavings(price.prices.intervals.monthly, price.prices.intervals.yearly) }}% with
annual billing!
</p>
<ButtonStyled
v-if="auth.user && isPermission(auth.user.badges, 1 << 0)"
color="purple"
size="large"
>
<ButtonStyled v-if="auth.user && hasActiveMidas(auth.user)" color="purple" size="large">
<nuxt-link to="/settings/billing">
<SettingsIcon aria-hidden="true" />
Manage subscription
@@ -95,8 +91,8 @@ import {
import { calculateSavings, getCurrency } from '@modrinth/utils'
import { useBaseFetch } from '@/composables/fetch.js'
import { isPermission } from '@/utils/permissions.ts'
import { products } from '~/generated/state.json'
import { hasActiveMidas } from '~/utils/user-membership.ts'
const { addNotification } = injectNotificationManager()
const formatPrice = useFormatPrice()
+28 -75
View File
@@ -460,8 +460,11 @@
</div>
</div>
<div class="normal-page__sidebar">
<div v-if="organizations?.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">
<div
v-if="organizations?.length > 0"
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
>
<h2 class="m-0 mb-2 text-lg text-contrast">
{{ formatMessage(messages.profileOrganizations) }}
</h2>
<div class="flex flex-wrap gap-2">
@@ -476,24 +479,16 @@
</nuxt-link>
</div>
</div>
<div v-if="badges.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">
{{ formatMessage(messages.profileBadges) }}
</h2>
<div class="flex flex-wrap gap-2">
<div v-for="badge in badges" :key="badge">
<StaffBadge v-if="badge === 'staff'" class="h-14 w-14" />
<ModBadge v-else-if="badge === 'mod'" class="h-14 w-14" />
<nuxt-link v-else-if="badge === 'plus'" to="/plus">
<PlusBadge class="h-14 w-14" />
</nuxt-link>
<TenMClubBadge v-else-if="badge === '10m-club'" class="h-14 w-14" />
<EarlyAdopterBadge v-else-if="badge === 'early-adopter'" class="h-14 w-14" />
<AlphaTesterBadge v-else-if="badge === 'alpha-tester'" class="h-14 w-14" />
<BetaTesterBadge v-else-if="badge === 'beta-tester'" class="h-14 w-14" />
</div>
</div>
</div>
<UserBadges
:downloads="sumDownloads"
:join-date="joinDate"
:role="user.role"
:badges="user.badges"
:has-midas="hasActiveMidas(user)"
:has-pride="hasPride26Badge(user)"
:earliest-project-by-type="earliestProjectByType"
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
/>
<AdPlaceholder v-if="!auth.user" />
</div>
</div>
@@ -540,6 +535,7 @@ import {
useCompactNumber,
useFormatDateTime,
useFormatNumber,
UserBadges,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
@@ -547,19 +543,13 @@ import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
import AlphaTesterBadge from '~/assets/images/badges/alpha-tester.svg?component'
import BetaTesterBadge from '~/assets/images/badges/beta-tester.svg?component'
import EarlyAdopterBadge from '~/assets/images/badges/early-adopter.svg?component'
import ModBadge from '~/assets/images/badges/mod.svg?component'
import PlusBadge from '~/assets/images/badges/plus.svg?component'
import StaffBadge from '~/assets/images/badges/staff.svg?component'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import { getSignInRouteObj } from '~/composables/auth.js'
import { reportUser } from '~/utils/report-helpers.ts'
import { hasActiveMidas, hasPride26Badge } from '~/utils/user-membership.ts'
const data = useNuxtApp()
const route = useNativeRoute()
@@ -743,7 +733,7 @@ const {
suspense: userSuspense,
} = useQuery({
queryKey: computed(() => ['user', userId]),
queryFn: () => client.labrinth.users_v2.get(userId),
queryFn: () => client.labrinth.users_v3.get(userId),
})
watch(
@@ -859,57 +849,21 @@ const sumDownloads = computed(() => {
})
const joinDate = computed(() => new Date(user.value.created))
const MODRINTH_BETA_END_DATE = new Date('2022-02-27T08:00:00.000Z')
const MODRINTH_ALPHA_END_DATE = new Date('2020-11-30T08:00:00.000Z')
const badges = computed(() => {
const badges = []
if (user.value.role === 'admin') {
badges.push('staff')
}
if (user.value.role === 'moderator') {
badges.push('mod')
}
if (isPermission(user.value.badges, 1 << 0)) {
badges.push('plus')
}
if (sumDownloads.value > 10000000) {
badges.push('10m-club')
}
if (
isPermission(user.value.badges, 1 << 1) ||
isPermission(user.value.badges, 1 << 2) ||
isPermission(user.value.badges, 1 << 3)
) {
badges.push('early-adopter')
}
if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_ALPHA_END_DATE) {
badges.push('alpha-tester')
} else if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_BETA_END_DATE) {
badges.push('beta-tester')
}
if (isPermission(user.value.badges, 1 << 5)) {
badges.push('contributor')
}
if (isPermission(user.value.badges, 1 << 6)) {
badges.push('translator')
}
return badges
})
async function copyId() {
await navigator.clipboard.writeText(user.value.id)
}
const earliestProjectByType = computed(() => {
const obj = {}
for (const project of projects.value ?? []) {
obj[project.project_type] = new Date(project.published)
}
return obj
})
async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
}
@@ -1012,7 +966,6 @@ export default defineNuxtComponent({
}
.description {
// Grow to take up remaining space
flex-grow: 1;
color: var(--color-text);