Affiliates frontend (#4380)

* Begin affiliates frontend

* Significant work on hooking up affiliates ui

* Clean up server nodes menu

* affiliates work

* update affiliate time

* oops

* fix local import

* fix local import x2

* remove line in dashboard

* lint
This commit is contained in:
Prospector
2025-11-02 11:32:18 -08:00
committed by GitHub
parent b7f0988399
commit 40cbe92dbc
33 changed files with 1202 additions and 37 deletions

View File

@@ -109,7 +109,18 @@
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
</template>
<template #title>
{{ user.username }}
<span class="flex items-center gap-2">
{{ user.username }}
<TagItem
v-if="isAdminViewing && isAffiliate"
:style="{
'--_color': 'var(--color-brand)',
'--_bg-color': 'var(--color-brand-highlight)',
}"
>
<AffiliateIcon /> Affiliate
</TagItem>
</span>
</template>
<template #summary>
{{
@@ -191,6 +202,13 @@
action: () => navigateTo(`/admin/billing/${user.id}`),
shown: auth.user && isStaff(auth.user),
},
{
id: 'toggle-affiliate',
action: () => toggleAffiliate(user.id),
shown: isAdminViewing,
remainOnClick: true,
color: isAffiliate ? 'red' : 'orange',
},
{
id: 'open-info',
action: () => $refs.userDetailsModal.show(),
@@ -203,6 +221,7 @@
},
]"
aria-label="More options"
:dropdown-id="`${baseId}-more-options`"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
@@ -229,6 +248,14 @@
<InfoIcon aria-hidden="true" />
{{ formatMessage(messages.infoButton) }}
</template>
<template #toggle-affiliate>
<AffiliateIcon aria-hidden="true" />
{{
formatMessage(
isAffiliate ? messages.removeAffiliateButton : messages.setAffiliateButton,
)
}}
</template>
<template #edit-role>
<EditIcon aria-hidden="true" />
{{ formatMessage(messages.editRoleButton) }}
@@ -411,6 +438,7 @@
</template>
<script setup>
import {
AffiliateIcon,
BoxIcon,
CalendarIcon,
CheckIcon,
@@ -437,10 +465,11 @@ import {
injectNotificationManager,
NewModal,
OverflowMenu,
TagItem,
TeleportDropdownMenu,
useRelativeTime,
} from '@modrinth/ui'
import { isAdmin } from '@modrinth/utils'
import { isAdmin, UserBadge } from '@modrinth/utils'
import { IntlFormatted } from '@vintl/vintl/components'
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
@@ -475,6 +504,8 @@ const formatRelativeTime = useRelativeTime()
const { addNotification } = injectNotificationManager()
const baseId = useId()
const messages = defineMessages({
profileProjectsLabel: {
id: 'profile.label.projects',
@@ -599,6 +630,18 @@ const messages = defineMessages({
id: 'profile.button.info',
defaultMessage: 'View user details',
},
setAffiliateButton: {
id: 'profile.button.set-affiliate',
defaultMessage: 'Set as affiliate',
},
removeAffiliateButton: {
id: 'profile.button.remove-affiliate',
defaultMessage: 'Remove as affiliate',
},
affiliateLabel: {
id: 'profile.label.affiliate',
defaultMessage: 'Affiliate',
},
editRoleButton: {
id: 'profile.button.edit-role',
defaultMessage: 'Edit role',
@@ -609,38 +652,42 @@ const messages = defineMessages({
},
})
let user, projects, organizations, collections
let user, projects, organizations, collections, refreshUser
try {
;[{ data: user }, { data: projects }, { data: organizations }, { data: collections }] =
await Promise.all([
useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
useAsyncData(
`user/${route.params.id}/projects`,
() => useBaseFetch(`user/${route.params.id}/projects`),
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories,
tags.value,
)
}
;[
{ data: user, refresh: refreshUser },
{ data: projects },
{ data: organizations },
{ data: collections },
] = await Promise.all([
useAsyncData(`user/${route.params.id}`, () => useBaseFetch(`user/${route.params.id}`)),
useAsyncData(
`user/${route.params.id}/projects`,
() => useBaseFetch(`user/${route.params.id}/projects`),
{
transform: (projects) => {
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories,
tags.value,
)
}
return projects
},
return projects
},
),
useAsyncData(`user/${route.params.id}/organizations`, () =>
useBaseFetch(`user/${route.params.id}/organizations`, {
apiVersion: 3,
}),
),
useAsyncData(`user/${route.params.id}/collections`, () =>
useBaseFetch(`user/${route.params.id}/collections`, { apiVersion: 3 }),
),
])
},
),
useAsyncData(`user/${route.params.id}/organizations`, () =>
useBaseFetch(`user/${route.params.id}/organizations`, {
apiVersion: 3,
}),
),
useAsyncData(`user/${route.params.id}/collections`, () =>
useBaseFetch(`user/${route.params.id}/collections`, { apiVersion: 3 }),
),
])
} catch {
throw createError({
fatal: true,
@@ -764,6 +811,17 @@ async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
}
const isAffiliate = computed(() => user.value.badges & UserBadge.AFFILIATE)
const isAdminViewing = computed(() => isAdmin(auth.value.user))
async function toggleAffiliate(id) {
await useBaseFetch(`user/${id}`, {
method: 'PATCH',
body: { badges: user.value.badges ^ (1 << 7) },
})
refreshUser()
}
const navLinks = computed(() => [
{
label: formatMessage(commonMessages.allProjectType),