Files
modrinth/apps/frontend/src/pages/dashboard.vue
T
Calum H. 543d25e2d6 feat: use discord_id from discord sso for role grant (#6326)
* feat: properly use labrinth's linked discord accts sso for discord bot

* Update email for fixture user in SQL insert

Signed-off-by: Calum H. <calum@modrinth.com>

* fix: rev changes

* fix: copy on email

* fix: lint

* fix: rev

* fix: lint

---------

Signed-off-by: Calum H. <calum@modrinth.com>
2026-06-09 17:33:07 +00:00

251 lines
6.4 KiB
Vue

<template>
<div class="normal-page !mt-8">
<div class="normal-page__sidebar">
<NavStack
:items="[
{ type: 'heading', label: formatMessage(messages.dashboard) },
{ link: '/dashboard', label: formatMessage(messages.overview), icon: DashboardIcon },
{
link: '/dashboard/notifications',
label: formatMessage(messages.notifications),
icon: NotificationsIcon,
},
{
link: '/dashboard/reports',
label: formatMessage(messages.activeReports),
icon: ReportIcon,
},
{
link: '/dashboard/collections',
label: formatMessage(commonMessages.collectionsLabel),
icon: LibraryIcon,
},
{ type: 'heading', label: formatMessage(messages.creators) },
{ link: '/dashboard/projects', label: formatMessage(messages.projects), icon: ListIcon },
{
link: '/dashboard/organizations',
label: formatMessage(messages.organizations),
icon: OrganizationIcon,
},
{
link: '/dashboard/analytics',
label: formatMessage(messages.analytics),
icon: ChartIcon,
},
{
link: '/dashboard/affiliate-links',
label: formatMessage(commonMessages.affiliateLinksButton),
icon: AffiliateIcon,
shown: !!isAffiliate,
},
{
link: '/dashboard/revenue',
label: formatMessage(messages.revenue),
icon: CurrencyIcon,
matchNested: true,
},
]"
/>
</div>
<div class="normal-page__content mt-4 lg:!mt-0">
<Admonition
v-if="showDiscordRoleBanner"
class="mb-3"
type="info"
:header="formatMessage(messages.discordRoleBannerTitle)"
show-actions-underneath
dismissible
@dismiss="dismissDiscordRoleBanner"
>
<div class="text-primary">
{{
formatMessage(messages.discordRoleBannerBody, {
roles: eligibleDiscordRolesLabel,
})
}}
</div>
<template #actions>
<ButtonStyled color="blue">
<NuxtLink to="/discord/link" class="w-fit !px-4">
<ExternalIcon />
{{ formatMessage(messages.discordRoleBannerCta) }}
</NuxtLink>
</ButtonStyled>
</template>
</Admonition>
<NuxtPage :route="route" />
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
AffiliateIcon,
BellIcon as NotificationsIcon,
ChartIcon,
CurrencyIcon,
DashboardIcon,
ExternalIcon,
LibraryIcon,
ListIcon,
OrganizationIcon,
ReportIcon,
} from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
defineMessages,
injectModrinthClient,
useVIntl,
} from '@modrinth/ui'
import { UserBadge } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { useLocalStorage } from '@vueuse/core'
import NavStack from '~/components/ui/NavStack.vue'
const auth = (await useAuth()) as Ref<{ user: Labrinth.Users.v3.User | null }>
const client = injectModrinthClient()
const dismissedDiscordRoleBannerUsers = useLocalStorage<string[]>(
'dashboard-discord-role-banner-dismissed-users',
[],
)
const isAffiliate = computed(() => {
return auth.value.user && auth.value.user.badges & UserBadge.AFFILIATE
})
const { formatMessage } = useVIntl()
const messages = defineMessages({
dashboard: {
id: 'dashboard.sidebar.label.dashboard',
defaultMessage: 'Dashboard',
},
overview: {
id: 'dashboard.sidebar.label.overview',
defaultMessage: 'Overview',
},
notifications: {
id: 'dashboard.sidebar.label.notifications',
defaultMessage: 'Notifications',
},
activeReports: {
id: 'dashboard.sidebar.label.activeReports',
defaultMessage: 'Active reports',
},
creators: {
id: 'dashboard.sidebar.label.creators',
defaultMessage: 'Creators',
},
projects: {
id: 'dashboard.sidebar.label.projects',
defaultMessage: 'Projects',
},
organizations: {
id: 'dashboard.sidebar.label.organizations',
defaultMessage: 'Organizations',
},
analytics: {
id: 'dashboard.sidebar.label.analytics',
defaultMessage: 'Analytics',
},
revenue: {
id: 'dashboard.sidebar.label.revenue',
defaultMessage: 'Revenue',
},
discordRoleBannerTitle: {
id: 'dashboard.discord-roles.banner.title',
defaultMessage: 'Claim your Discord roles',
},
discordRoleBannerBody: {
id: 'dashboard.discord-roles.banner.body',
defaultMessage:
"You're eligible for {roles}. Link your Discord account through Modrinth and we'll sync them automatically.",
},
discordRoleBannerCta: {
id: 'dashboard.discord-roles.banner.cta',
defaultMessage: 'Link Discord',
},
discordRolePride: {
id: 'dashboard.discord-roles.role.pride',
defaultMessage: 'Pride 2026',
},
discordRoleCreator: {
id: 'dashboard.discord-roles.role.creator',
defaultMessage: 'Creator',
},
discordRoleBigCreator: {
id: 'dashboard.discord-roles.role.big-creator',
defaultMessage: '1M+ Downloads',
},
})
definePageMeta({
middleware: 'auth',
})
useSeoMeta({
robots: 'noindex',
})
const route = useNativeRoute()
const { data: projects } = useQuery({
queryKey: computed(() => ['dashboard-discord-role-eligibility', auth.value.user?.id, 'projects']),
queryFn: () => {
const userId = auth.value.user?.id
if (!userId) return []
return client.labrinth.users_v2.getProjects(userId)
},
enabled: computed(() => !!auth.value.user?.id),
})
const totalProjectDownloads = computed(() =>
(projects.value ?? []).reduce((total, project) => total + (project.downloads ?? 0), 0),
)
const eligibleDiscordRoles = computed(() => {
const roles = []
if (auth.value.user?.campaigns?.pride_26?.has_badge === true) {
roles.push(formatMessage(messages.discordRolePride))
}
if (totalProjectDownloads.value >= 20_000) {
roles.push(formatMessage(messages.discordRoleCreator))
}
if (totalProjectDownloads.value >= 1_000_000) {
roles.push(formatMessage(messages.discordRoleBigCreator))
}
return roles
})
const roleListFormatter = new Intl.ListFormat(undefined, {
style: 'long',
type: 'conjunction',
})
const eligibleDiscordRolesLabel = computed(() =>
roleListFormatter.format(eligibleDiscordRoles.value),
)
const hasDismissedDiscordRoleBanner = computed(() =>
dismissedDiscordRoleBannerUsers.value.includes(auth.value.user?.id ?? ''),
)
const showDiscordRoleBanner = computed(
() => eligibleDiscordRoles.value.length > 0 && !hasDismissedDiscordRoleBanner.value,
)
function dismissDiscordRoleBanner() {
const userId = auth.value.user?.id
if (!userId || dismissedDiscordRoleBannerUsers.value.includes(userId)) return
dismissedDiscordRoleBannerUsers.value = [...dismissedDiscordRoleBannerUsers.value, userId]
}
</script>