diff --git a/apps/app-frontend/src/providers/setup/auth.ts b/apps/app-frontend/src/providers/setup/auth.ts
index 2de8c0acf..8832c09c6 100644
--- a/apps/app-frontend/src/providers/setup/auth.ts
+++ b/apps/app-frontend/src/providers/setup/auth.ts
@@ -1,5 +1,5 @@
import type { Labrinth } from '@modrinth/api-client'
-import { type AuthProvider, provideAuth } from '@modrinth/ui'
+import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
import { computed, type Ref, ref, watchEffect } from 'vue'
type AppCredentials = {
@@ -12,7 +12,7 @@ export function setupAuthProvider(
requestSignIn: (redirectPath: string) => void | Promise,
) {
const sessionToken = ref(null)
- const user = ref(null)
+ const user = ref(null)
const isReady = computed(() => credentials.value !== undefined)
const authProvider: AuthProvider = {
diff --git a/apps/app-frontend/vite.config.ts b/apps/app-frontend/vite.config.ts
index 5db05f562..d97b76d8f 100644
--- a/apps/app-frontend/vite.config.ts
+++ b/apps/app-frontend/vite.config.ts
@@ -58,6 +58,9 @@ export default defineConfig({
params: {
overrides: {
removeViewBox: false,
+ cleanupIds: {
+ minify: false,
+ },
},
},
},
diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts
index 6a05ce659..83f428f01 100644
--- a/apps/frontend/nuxt.config.ts
+++ b/apps/frontend/nuxt.config.ts
@@ -104,6 +104,9 @@ export default defineNuxtConfig({
params: {
overrides: {
removeViewBox: false,
+ cleanupIds: {
+ minify: false,
+ },
},
},
},
diff --git a/apps/frontend/src/assets/styles/components.scss b/apps/frontend/src/assets/styles/components.scss
index cb9f0e2f8..6b91a25ff 100644
--- a/apps/frontend/src/assets/styles/components.scss
+++ b/apps/frontend/src/assets/styles/components.scss
@@ -1,18 +1,18 @@
/*
Cards and body styling
*/
+// CARDS
.base-card {
- padding: var(--spacing-card-lg);
+ padding: 1rem;
position: relative;
+ min-height: var(--font-size-2xl);
- background-color: var(--color-raised-bg);
- border-radius: var(--size-rounded-card);
+ background-color: var(--surface-3);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--surface-4);
- margin-bottom: var(--spacing-card-md);
+ margin-bottom: var(--gap-md);
outline: 2px solid transparent;
- outline-offset: -2px;
-
- box-shadow: var(--shadow-card);
.card__overlay {
position: absolute;
@@ -25,6 +25,17 @@
z-index: 2;
}
+ &:where(&.warning, &.information) {
+ padding: 1.5rem;
+ line-height: 1.5;
+ min-height: 0;
+
+ a {
+ color: var(--color-blue);
+ text-decoration: underline;
+ }
+ }
+
&.moderation-card {
background-color: var(--color-warning-banner-bg);
}
diff --git a/apps/frontend/src/components/ui/AdPlaceholder.vue b/apps/frontend/src/components/ui/AdPlaceholder.vue
index 01f25ead1..7c7737520 100644
--- a/apps/frontend/src/components/ui/AdPlaceholder.vue
+++ b/apps/frontend/src/components/ui/AdPlaceholder.vue
@@ -3,7 +3,7 @@
diff --git a/apps/frontend/src/components/ui/NavStack.vue b/apps/frontend/src/components/ui/NavStack.vue
index 4e2735d19..16bef5bbb 100644
--- a/apps/frontend/src/components/ui/NavStack.vue
+++ b/apps/frontend/src/components/ui/NavStack.vue
@@ -1,7 +1,8 @@
-
+
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()
diff --git a/apps/frontend/src/pages/user/[user].vue b/apps/frontend/src/pages/user/[user].vue
index e563043b0..a621b254c 100644
--- a/apps/frontend/src/pages/user/[user].vue
+++ b/apps/frontend/src/pages/user/[user].vue
@@ -460,8 +460,11 @@
@@ -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);
diff --git a/apps/frontend/src/providers/setup/auth.ts b/apps/frontend/src/providers/setup/auth.ts
index cb15021c4..5dccd4f6d 100644
--- a/apps/frontend/src/providers/setup/auth.ts
+++ b/apps/frontend/src/providers/setup/auth.ts
@@ -1,5 +1,5 @@
import type { Labrinth } from '@modrinth/api-client'
-import { type AuthProvider, provideAuth } from '@modrinth/ui'
+import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
import { ref, watchEffect } from 'vue'
import { getSignInRedirectPath } from '~/composables/auth.js'
@@ -8,7 +8,7 @@ export function setupAuthProvider(auth: Awaited>) {
const router = useRouter()
const route = useRoute()
const sessionToken = ref(null)
- const user = ref(null)
+ const user = ref(null)
const authProvider: AuthProvider = {
session_token: sessionToken,
@@ -26,7 +26,7 @@ export function setupAuthProvider(auth: Awaited>) {
watchEffect(() => {
sessionToken.value = auth.value.token || null
- user.value = (auth.value.user as Labrinth.Users.v2.User | null) ?? null
+ user.value = (auth.value.user as Labrinth.Users.v3.User | null) ?? null
})
provideAuth(authProvider)
diff --git a/apps/frontend/src/utils/user-membership.ts b/apps/frontend/src/utils/user-membership.ts
new file mode 100644
index 000000000..d4f19707c
--- /dev/null
+++ b/apps/frontend/src/utils/user-membership.ts
@@ -0,0 +1,40 @@
+import { UserBadge } from '@modrinth/utils'
+
+const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
+
+type Pride26Campaign = {
+ last_donated_at?: string | null
+ has_badge?: boolean | null
+ has_midas?: boolean | null
+}
+
+type UserWithMembership = {
+ badges?: number | null
+ campaigns?: {
+ pride_26?: Pride26Campaign | null
+ } | null
+}
+
+export function hasPride26Badge(user?: UserWithMembership | null) {
+ return user?.campaigns?.pride_26?.has_badge === true
+}
+
+export function hasActivePride26Midas(user?: UserWithMembership | null, now = Date.now()) {
+ const pride26Campaign = user?.campaigns?.pride_26
+
+ if (!pride26Campaign?.has_midas || !pride26Campaign.last_donated_at) {
+ return false
+ }
+
+ const lastDonatedAt = Date.parse(pride26Campaign.last_donated_at)
+
+ if (!Number.isFinite(lastDonatedAt)) {
+ return false
+ }
+
+ return lastDonatedAt + PRIDE_26_MIDAS_DURATION_MS > now
+}
+
+export function hasActiveMidas(user?: UserWithMembership | null, now = Date.now()) {
+ return Boolean((user?.badges ?? 0) & UserBadge.MIDAS) || hasActivePride26Midas(user, now)
+}
diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts
index 2a3600868..b582a5f34 100644
--- a/packages/api-client/src/modules/labrinth/types.ts
+++ b/packages/api-client/src/modules/labrinth/types.ts
@@ -1298,6 +1298,16 @@ export namespace Labrinth {
export type AuthProvider = Common.AuthProvider
export type UserPayoutData = Common.UserPayoutData
+ export type Pride26CampaignDonation = {
+ last_donated_at: string
+ has_badge: boolean
+ has_midas: boolean
+ }
+
+ export type UserCampaigns = {
+ pride_26: Pride26CampaignDonation | null
+ }
+
export type User = {
id: string
username: string
@@ -1314,6 +1324,7 @@ export namespace Labrinth {
payout_data?: UserPayoutData
stripe_customer_id?: string
allow_friend_requests?: boolean
+ campaigns: UserCampaigns
github_id?: number
}
diff --git a/packages/api-client/src/modules/labrinth/users/v3.ts b/packages/api-client/src/modules/labrinth/users/v3.ts
index cdc4d350c..92bc2557c 100644
--- a/packages/api-client/src/modules/labrinth/users/v3.ts
+++ b/packages/api-client/src/modules/labrinth/users/v3.ts
@@ -6,6 +6,25 @@ export class LabrinthUsersV3Module extends AbstractModule {
return 'labrinth_users_v3'
}
+ /**
+ * Get a user by ID or username
+ *
+ * @param idOrUsername - The user's ID or username
+ * @returns Promise resolving to the user data
+ *
+ * GET /v3/user/{id}
+ */
+ public async get(idOrUsername: string): Promise {
+ return this.client.request(
+ `/user/${encodeURIComponent(idOrUsername)}`,
+ {
+ api: 'labrinth',
+ version: 3,
+ method: 'GET',
+ },
+ )
+ }
+
/**
* Get all projects the authenticated user can access directly or through
* their organizations.
diff --git a/packages/assets/build/generate-exports.ts b/packages/assets/build/generate-exports.ts
index ed73cfc67..a49d494ea 100644
--- a/packages/assets/build/generate-exports.ts
+++ b/packages/assets/build/generate-exports.ts
@@ -121,6 +121,30 @@ function generateIconExports(): {
})
}
+ // Process badge icons from icons/badges/
+ const badgesDir = path.join(iconsDir, 'badges')
+ if (fs.existsSync(badgesDir)) {
+ const badgeFiles = fs.readdirSync(badgesDir).filter((file) => file.endsWith('.svg'))
+ badgeFiles.forEach((file) => {
+ const baseName = path.basename(file, '.svg')
+ let pascalName = toPascalCase(baseName)
+
+ if (pascalName === '') {
+ pascalName = 'Unknown'
+ }
+
+ if (!pascalName.endsWith('Badge')) {
+ pascalName += 'Badge'
+ }
+
+ icons.push({
+ importPath: `./icons/badges/${file}?component`,
+ pascalName,
+ privateName: `_${pascalName}`,
+ })
+ })
+ }
+
// Sort by import path using simple-import-sort's algorithm
icons.sort((a, b) => compareImportSources(a.importPath, b.importPath))
@@ -156,7 +180,7 @@ function generateIconExports(): {
function runTests(): void {
console.log('🧪 Running conversion tests...\n')
- const testCases: Array<{ input: string; expected: string }> = [
+ const testCases: Array<{ input: string; expected: string; suffix?: string }> = [
{ input: 'align-left', expected: 'AlignLeftIcon' },
{ input: 'arrow-big-up-dash', expected: 'ArrowBigUpDashIcon' },
{ input: 'check-check', expected: 'CheckCheckIcon' },
@@ -171,13 +195,17 @@ function runTests(): void {
{ input: 'list_bulleted', expected: 'ListBulletedIcon' },
{ input: 'test.name', expected: 'TestNameIcon' },
{ input: 'test-name_final.icon', expected: 'TestNameFinalIcon' },
+ { input: 'downloads-500m', expected: 'Downloads500mBadge', suffix: 'Badge' },
+ { input: 'early-modpack', expected: 'EarlyModpackBadge', suffix: 'Badge' },
+ { input: 'plus', expected: 'PlusBadge', suffix: 'Badge' },
]
let passed = 0
let failed = 0
- testCases.forEach(({ input, expected }) => {
- const result = toPascalCase(input) + (toPascalCase(input).endsWith('Icon') ? '' : 'Icon')
+ testCases.forEach(({ input, expected, suffix = 'Icon' }) => {
+ const base = toPascalCase(input)
+ const result = base.endsWith(suffix) ? base : base + suffix
const success = result === expected
if (success) {
@@ -312,6 +340,26 @@ function getExpectedIconExports(iconsDir: string): string[] {
})
}
+ // Process badge icons from icons/badges/
+ const badgesDir = path.join(iconsDir, 'badges')
+ if (fs.existsSync(badgesDir)) {
+ const badgeFiles = fs.readdirSync(badgesDir).filter((file) => file.endsWith('.svg'))
+ badgeFiles.forEach((file) => {
+ const baseName = path.basename(file, '.svg')
+ let pascalName = toPascalCase(baseName)
+
+ if (pascalName === '') {
+ pascalName = 'Unknown'
+ }
+
+ if (!pascalName.endsWith('Badge')) {
+ pascalName += 'Badge'
+ }
+
+ exports.push(pascalName)
+ })
+ }
+
return exports.sort()
}
@@ -321,14 +369,15 @@ function getActualIconExports(indexFile: string): string[] {
}
const content = fs.readFileSync(indexFile, 'utf8')
- const exportMatches = content.match(/export const (\w+Icon) = _\w+Icon/g) || []
+ const exportMatches =
+ content.match(/export const (\w+(?:Icon|Badge)) = _\w+(?:Icon|Badge)/g) || []
return exportMatches
.map((match) => {
- const result = match.match(/export const (\w+Icon)/)
+ const result = match.match(/export const (\w+(?:Icon|Badge))/)
return result ? result[1] : ''
})
- .filter((name) => name.endsWith('Icon'))
+ .filter((name) => name.endsWith('Icon') || name.endsWith('Badge'))
.sort()
}
diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts
index 400abf1e9..6f8549ccb 100644
--- a/packages/assets/generated-icons.ts
+++ b/packages/assets/generated-icons.ts
@@ -23,6 +23,26 @@ import _ArrowUpZAIcon from './icons/arrow-up-z-a.svg?component'
import _AsteriskIcon from './icons/asterisk.svg?component'
import _BadgeCheckIcon from './icons/badge-check.svg?component'
import _BadgeDollarSignIcon from './icons/badge-dollar-sign.svg?component'
+import _AlphaBadge from './icons/badges/alpha.svg?component'
+import _BetaBadge from './icons/badges/beta.svg?component'
+import _Downloads1mBadge from './icons/badges/downloads-1m.svg?component'
+import _Downloads10mBadge from './icons/badges/downloads-10m.svg?component'
+import _Downloads25mBadge from './icons/badges/downloads-25m.svg?component'
+import _Downloads50mBadge from './icons/badges/downloads-50m.svg?component'
+import _Downloads100mBadge from './icons/badges/downloads-100m.svg?component'
+import _Downloads250mBadge from './icons/badges/downloads-250m.svg?component'
+import _Downloads500mBadge from './icons/badges/downloads-500m.svg?component'
+import _EarlyDatapackBadge from './icons/badges/early-datapack.svg?component'
+import _EarlyHostingBadge from './icons/badges/early-hosting.svg?component'
+import _EarlyModpackBadge from './icons/badges/early-modpack.svg?component'
+import _EarlyPluginBadge from './icons/badges/early-plugin.svg?component'
+import _EarlyResourcepackBadge from './icons/badges/early-resourcepack.svg?component'
+import _EarlyServersBadge from './icons/badges/early-servers.svg?component'
+import _EarlyShadersBadge from './icons/badges/early-shaders.svg?component'
+import _ModeratorBadge from './icons/badges/moderator.svg?component'
+import _PlusBadge from './icons/badges/plus.svg?component'
+import _PrideBadge from './icons/badges/pride.svg?component'
+import _StaffBadge from './icons/badges/staff.svg?component'
import _BanIcon from './icons/ban.svg?component'
import _BellIcon from './icons/bell.svg?component'
import _BellRingIcon from './icons/bell-ring.svg?component'
@@ -423,6 +443,26 @@ export const ArrowUpZAIcon = _ArrowUpZAIcon
export const AsteriskIcon = _AsteriskIcon
export const BadgeCheckIcon = _BadgeCheckIcon
export const BadgeDollarSignIcon = _BadgeDollarSignIcon
+export const AlphaBadge = _AlphaBadge
+export const BetaBadge = _BetaBadge
+export const Downloads1mBadge = _Downloads1mBadge
+export const Downloads10mBadge = _Downloads10mBadge
+export const Downloads25mBadge = _Downloads25mBadge
+export const Downloads50mBadge = _Downloads50mBadge
+export const Downloads100mBadge = _Downloads100mBadge
+export const Downloads250mBadge = _Downloads250mBadge
+export const Downloads500mBadge = _Downloads500mBadge
+export const EarlyDatapackBadge = _EarlyDatapackBadge
+export const EarlyHostingBadge = _EarlyHostingBadge
+export const EarlyModpackBadge = _EarlyModpackBadge
+export const EarlyPluginBadge = _EarlyPluginBadge
+export const EarlyResourcepackBadge = _EarlyResourcepackBadge
+export const EarlyServersBadge = _EarlyServersBadge
+export const EarlyShadersBadge = _EarlyShadersBadge
+export const ModeratorBadge = _ModeratorBadge
+export const PlusBadge = _PlusBadge
+export const PrideBadge = _PrideBadge
+export const StaffBadge = _StaffBadge
export const BanIcon = _BanIcon
export const BellIcon = _BellIcon
export const BellRingIcon = _BellRingIcon
diff --git a/packages/assets/icons/badges/alpha.svg b/packages/assets/icons/badges/alpha.svg
new file mode 100644
index 000000000..dbd5cc042
--- /dev/null
+++ b/packages/assets/icons/badges/alpha.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/beta.svg b/packages/assets/icons/badges/beta.svg
new file mode 100644
index 000000000..03df84ab5
--- /dev/null
+++ b/packages/assets/icons/badges/beta.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-100m.svg b/packages/assets/icons/badges/downloads-100m.svg
new file mode 100644
index 000000000..64896442a
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-100m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-10m.svg b/packages/assets/icons/badges/downloads-10m.svg
new file mode 100644
index 000000000..83b3e10ad
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-10m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-1m.svg b/packages/assets/icons/badges/downloads-1m.svg
new file mode 100644
index 000000000..c15ae5bd8
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-1m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-250m.svg b/packages/assets/icons/badges/downloads-250m.svg
new file mode 100644
index 000000000..84cbd4f4a
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-250m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-25m.svg b/packages/assets/icons/badges/downloads-25m.svg
new file mode 100644
index 000000000..6224206d0
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-25m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-500m.svg b/packages/assets/icons/badges/downloads-500m.svg
new file mode 100644
index 000000000..dc58c96ab
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-500m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/downloads-50m.svg b/packages/assets/icons/badges/downloads-50m.svg
new file mode 100644
index 000000000..30bbdd69e
--- /dev/null
+++ b/packages/assets/icons/badges/downloads-50m.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-datapack.svg b/packages/assets/icons/badges/early-datapack.svg
new file mode 100644
index 000000000..795e1a309
--- /dev/null
+++ b/packages/assets/icons/badges/early-datapack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-hosting.svg b/packages/assets/icons/badges/early-hosting.svg
new file mode 100644
index 000000000..bec8fe83a
--- /dev/null
+++ b/packages/assets/icons/badges/early-hosting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-modpack.svg b/packages/assets/icons/badges/early-modpack.svg
new file mode 100644
index 000000000..9b337d6bb
--- /dev/null
+++ b/packages/assets/icons/badges/early-modpack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-plugin.svg b/packages/assets/icons/badges/early-plugin.svg
new file mode 100644
index 000000000..2a5766812
--- /dev/null
+++ b/packages/assets/icons/badges/early-plugin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-resourcepack.svg b/packages/assets/icons/badges/early-resourcepack.svg
new file mode 100644
index 000000000..6c7bc7048
--- /dev/null
+++ b/packages/assets/icons/badges/early-resourcepack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-servers.svg b/packages/assets/icons/badges/early-servers.svg
new file mode 100644
index 000000000..f5667bd8e
--- /dev/null
+++ b/packages/assets/icons/badges/early-servers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/early-shaders.svg b/packages/assets/icons/badges/early-shaders.svg
new file mode 100644
index 000000000..1067ffca8
--- /dev/null
+++ b/packages/assets/icons/badges/early-shaders.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/moderator.svg b/packages/assets/icons/badges/moderator.svg
new file mode 100644
index 000000000..69ac88b4e
--- /dev/null
+++ b/packages/assets/icons/badges/moderator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/plus.svg b/packages/assets/icons/badges/plus.svg
new file mode 100644
index 000000000..a9780cbbb
--- /dev/null
+++ b/packages/assets/icons/badges/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/pride.svg b/packages/assets/icons/badges/pride.svg
new file mode 100644
index 000000000..d47be1dc0
--- /dev/null
+++ b/packages/assets/icons/badges/pride.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/icons/badges/staff.svg b/packages/assets/icons/badges/staff.svg
new file mode 100644
index 000000000..40443abab
--- /dev/null
+++ b/packages/assets/icons/badges/staff.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/assets/styles/classes.scss b/packages/assets/styles/classes.scss
index 1822c5fda..e2018245a 100644
--- a/packages/assets/styles/classes.scss
+++ b/packages/assets/styles/classes.scss
@@ -495,12 +495,13 @@ a:not(.no-click-animation),
// CARDS
.base-card {
- padding: var(--gap-xl);
+ padding: 1rem;
position: relative;
min-height: var(--font-size-2xl);
- background-color: var(--color-raised-bg);
+ background-color: var(--surface-3);
border-radius: var(--radius-lg);
+ border: 1px solid var(--surface-4);
margin-bottom: var(--gap-md);
outline: 2px solid transparent;
@@ -526,19 +527,6 @@ a:not(.no-click-animation),
text-decoration: underline;
}
}
-
- // TODO: Add back later
- //&.warning {
- // border-left: 0.5rem solid var(--color-warning-banner-side);
- // background-color: var(--color-warning-banner-bg);
- // color: var(--color-warning-banner-text);
- //}
- //
- //&.information {
- // border-left: 0.5rem solid var(--color-info-banner-side);
- // background-color: var(--color-info-banner-bg);
- // color: var(--color-info-banner-text);
- //}
}
.card {
@@ -654,20 +642,20 @@ a:not(.no-click-animation),
.v-popper--theme-dropdown,
.v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
.v-popper__inner {
- border: 1px solid var(--color-divider) !important;
+ border: 1px solid var(--surface-5) !important;
padding: var(--gap-sm) !important;
width: fit-content !important;
- border-radius: var(--radius-md) !important;
- background-color: var(--color-raised-bg) !important;
- box-shadow: var(--shadow-floating) !important;
+ border-radius: 12px !important;
+ background-color: var(--surface-3) !important;
+ box-shadow: 3px 3px 0.8rem rgba(0, 0, 0, 0.3) !important;
}
.v-popper__arrow-outer {
- border-color: var(--color-divider) !important;
+ border-color: var(--surface-5) !important;
}
.v-popper__arrow-inner {
- border-color: var(--color-raised-bg) !important;
+ border-color: var(--surface-3) !important;
}
}
@@ -711,22 +699,25 @@ a:not(.no-click-animation),
}
.v-popper--theme-tooltip {
- pointer-events: none;
-
.v-popper__inner {
- background: var(--color-tooltip-bg) !important;
- color: var(--color-tooltip-text) !important;
- padding: 0.5rem 0.5rem !important;
- border-radius: var(--radius-sm) !important;
- filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
+ background: var(--surface-3) !important;
+ color: var(--color-contrast) !important;
+ padding: 0.625rem 0.75rem !important;
+ border-radius: 12px !important;
+ filter: drop-shadow(2px 2px 0.4rem rgba(0, 0, 0, 0.5));
font-size: 0.9rem;
- font-weight: bold;
+ font-weight: 500;
line-height: 1;
+ border: 1px solid var(--surface-5);
+ }
+
+ .v-popper__arrow-outer {
+ border-color: var(--surface-5) !important;
}
- .v-popper__arrow-outer,
.v-popper__arrow-inner {
- border-color: var(--color-tooltip-bg);
+ visibility: visible;
+ border-color: var(--surface-3) !important;
}
}
diff --git a/packages/ui/src/components/base/NavTabs.vue b/packages/ui/src/components/base/NavTabs.vue
index 76b095746..a4d45bf1b 100644
--- a/packages/ui/src/components/base/NavTabs.vue
+++ b/packages/ui/src/components/base/NavTabs.vue
@@ -3,7 +3,7 @@
v-if="filteredLinks.length > 1"
ref="scrollContainer"
class="relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
- :class="{ 'drop-shadow-xl': mode === 'navigation' }"
+ :class="{ 'drop-shadow-xl border border-solid border-surface-4': mode === 'navigation' }"
>
+import { ExternalIcon } from '@modrinth/assets'
+import { Tooltip } from 'floating-vue'
+import { type Component, useId } from 'vue'
+
+import { type MessageDescriptor, useVIntl } from '#ui/composables/i18n.ts'
+
+import AutoLink from '../base/AutoLink.vue'
+
+const { formatMessage } = useVIntl()
+
+defineProps<{
+ icon: Component
+ name: MessageDescriptor
+ about: MessageDescriptor[]
+ values?: Record
+ link?: {
+ href: string
+ message: MessageDescriptor
+ }
+}>()
+
+const baseId = useId()
+
+
+
+
+
+
+
+
+ {{ formatMessage(name, values) }}
+
+ {{ formatMessage(message, values) }}
+
+
+ {{ formatMessage(link.message, values) }}
+
+
+
+
+
diff --git a/packages/ui/src/components/user/UserBadges.vue b/packages/ui/src/components/user/UserBadges.vue
new file mode 100644
index 000000000..77fc2fc4d
--- /dev/null
+++ b/packages/ui/src/components/user/UserBadges.vue
@@ -0,0 +1,507 @@
+
+
+
+
+
+ {{ formatMessage(messages.title) }}
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/user/index.ts b/packages/ui/src/components/user/index.ts
new file mode 100644
index 000000000..1d760e312
--- /dev/null
+++ b/packages/ui/src/components/user/index.ts
@@ -0,0 +1 @@
+export { default as UserBadges } from './UserBadges.vue'
diff --git a/packages/ui/src/layouts/shared/browse-tab/sidebar.vue b/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
index 968c22a03..c8a4fedf8 100644
--- a/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
+++ b/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
@@ -30,14 +30,14 @@ const filterClass = computed(() => {
if (ctx.filtersMenuOpen?.value) {
return 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
}
- return 'card-shadow rounded-2xl bg-bg-raised'
+ return 'card-shadow rounded-2xl bg-surface-3 border border-solid border-surface-4'
})
const buttonClass = computed(() => {
if (isApp.value) {
- return 'button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
+ return 'button-animation flex flex-col gap-1 px-3 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
}
- return 'button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none'
+ return 'button-animation flex flex-col gap-1 px-6 py-3 w-full bg-transparent cursor-pointer border-none'
})
const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3'))
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index c7520ca2b..2d7f7f921 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -2423,6 +2423,9 @@
"payment-method.visa": {
"defaultMessage": "Visa"
},
+ "profile.label.badges": {
+ "defaultMessage": "Badges"
+ },
"project-card.date.published.tooltip": {
"defaultMessage": "Published {date}"
},
@@ -4483,5 +4486,101 @@
},
"ui.stacked-admonitions.dismiss-all": {
"defaultMessage": "Dismiss all"
+ },
+ "user.profile.badge.alpha.about.1": {
+ "defaultMessage": "This user has been around since Modrinth Alpha, which ended in November 2020"
+ },
+ "user.profile.badge.alpha.name": {
+ "defaultMessage": "Alpha Tester"
+ },
+ "user.profile.badge.beta.about.1": {
+ "defaultMessage": "This user has been around since Modrinth Beta, which ended in February 2022."
+ },
+ "user.profile.badge.beta.link": {
+ "defaultMessage": "Click to read about the launch of Modrinth Beta."
+ },
+ "user.profile.badge.beta.name": {
+ "defaultMessage": "Beta Tester"
+ },
+ "user.profile.badge.downloads.about.1": {
+ "defaultMessage": "This user's projects have collectively achieved {download_sum} downloads."
+ },
+ "user.profile.badge.downloads.name": {
+ "defaultMessage": "{download_sum} Downloads"
+ },
+ "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."
+ },
+ "user.profile.badge.early-datapack-adopter.name": {
+ "defaultMessage": "Early Data Pack Adopter"
+ },
+ "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."
+ },
+ "user.profile.badge.early-modpack-adopter.name": {
+ "defaultMessage": "Early Modpack Adopter"
+ },
+ "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."
+ },
+ "user.profile.badge.early-plugin-adopter.name": {
+ "defaultMessage": "Early Plugin Adopter"
+ },
+ "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."
+ },
+ "user.profile.badge.early-resourcepack-adopter.name": {
+ "defaultMessage": "Early Resource Pack Adopter"
+ },
+ "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."
+ },
+ "user.profile.badge.early-server-adopter.name": {
+ "defaultMessage": "Early Server Adopter"
+ },
+ "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."
+ },
+ "user.profile.badge.early-shader-adopter.name": {
+ "defaultMessage": "Early Shader Adopter"
+ },
+ "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"
+ },
+ "user.profile.badge.hosting-alpha.name": {
+ "defaultMessage": "Modrinth Hosting Alpha Tester"
+ },
+ "user.profile.badge.moderator.about.1": {
+ "defaultMessage": "This user works for Modrinth as a Content Moderator."
+ },
+ "user.profile.badge.moderator.about.2": {
+ "defaultMessage": "Content Moderators on Modrinth review projects, handle reports, and help keep Modrinth safe."
+ },
+ "user.profile.badge.moderator.name": {
+ "defaultMessage": "Content Moderator"
+ },
+ "user.profile.badge.plus.about.1": {
+ "defaultMessage": "This user is going the extra mile to support Modrinth and the creators on the platform."
+ },
+ "user.profile.badge.plus.link": {
+ "defaultMessage": "Click to learn more about how you can become a member."
+ },
+ "user.profile.badge.plus.name": {
+ "defaultMessage": "Modrinth+ Member"
+ },
+ "user.profile.badge.pride.about.1": {
+ "defaultMessage": "This user participated in at least one of Modrinth's Pride fundraisers for the LGBTQ+ community."
+ },
+ "user.profile.badge.pride.link": {
+ "defaultMessage": "Click to visit our latest Pride fundraiser."
+ },
+ "user.profile.badge.pride.name": {
+ "defaultMessage": "Pride Fundraiser Supporter"
+ },
+ "user.profile.badge.staff.about.1": {
+ "defaultMessage": "This user works for Modrinth."
+ },
+ "user.profile.badge.staff.name": {
+ "defaultMessage": "Modrinth Team"
}
}
diff --git a/packages/ui/src/providers/auth.ts b/packages/ui/src/providers/auth.ts
index e1bbc2032..b9de5eb62 100644
--- a/packages/ui/src/providers/auth.ts
+++ b/packages/ui/src/providers/auth.ts
@@ -3,9 +3,11 @@ import type { Ref } from 'vue'
import { createContext } from './create-context'
+export type AuthUser = Labrinth.Users.v2.User | Labrinth.Users.v3.User
+
export interface AuthProvider {
session_token: Ref
- user: Ref
+ user: Ref
/** True once the initial auth check has completed (regardless of result). */
isReady?: Ref
requestSignIn: (redirectPath: string) => void | Promise
diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts
index 6b9a9af53..e8d27d249 100644
--- a/packages/ui/vite.config.ts
+++ b/packages/ui/vite.config.ts
@@ -15,6 +15,9 @@ export default defineConfig({
params: {
overrides: {
removeViewBox: false,
+ cleanupIds: {
+ minify: false,
+ },
},
},
},