You've already forked AstralRinth
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<AutoLink
|
||||
:to="currentAd.link"
|
||||
:aria-label="currentAd.description"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit] bg-bg-raised"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit] border border-solid border-surface-4 bg-surface-3"
|
||||
>
|
||||
<img
|
||||
:src="currentAd.light"
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
</AutoLink>
|
||||
<div
|
||||
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl bg-bg-raised"
|
||||
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
>
|
||||
<div id="modrinth-rail-1" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<nav :aria-label="ariaLabel" class="w-full">
|
||||
<ul
|
||||
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"
|
||||
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
|
||||
:class="{ 'pt-3': filteredItems?.[0]?.type === 'heading' }"
|
||||
>
|
||||
<slot v-if="hasSlotContent" />
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const initAuth = async (oldToken = null) => {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
@@ -105,6 +106,7 @@ export const initAuth = async (oldToken = null) => {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
|
||||
@@ -793,6 +793,7 @@ import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { errors as generatedStateErrors } from '~/generated/state.json'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
import { hasActiveMidas } from '~/utils/user-membership.ts'
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
|
||||
@@ -1096,7 +1097,7 @@ const userMenuOptions = computed(() => {
|
||||
id: 'plus',
|
||||
link: '/plus',
|
||||
color: 'purple',
|
||||
shown: !flags.value.hidePlusPromoInUserMenu && !isPermission(user.badges, 1 << 0),
|
||||
shown: !flags.value.hidePlusPromoInUserMenu && !hasActiveMidas(user),
|
||||
},
|
||||
{
|
||||
id: 'servers',
|
||||
|
||||
@@ -2,19 +2,21 @@ import { useAppQueryClient } from '~/composables/query-client'
|
||||
import { useServerModrinthClient } from '~/server/utils/api-client'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
if (!to.path.startsWith('/user/') || !to.params.id) {
|
||||
const userParam = to.params.user ?? to.params.id
|
||||
const userId = Array.isArray(userParam) ? userParam[0] : userParam
|
||||
|
||||
if (!to.path.startsWith('/user/') || !userId) {
|
||||
return
|
||||
}
|
||||
|
||||
const queryClient = useAppQueryClient()
|
||||
const authToken = useCookie('auth-token')
|
||||
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
|
||||
const userId = to.params.id as string
|
||||
|
||||
try {
|
||||
const user = await queryClient.fetchQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => client.labrinth.users_v2.get(userId),
|
||||
queryFn: () => client.labrinth.users_v3.get(userId),
|
||||
})
|
||||
|
||||
if (!user) return
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ReturnType<typeof useAuth>>) {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const sessionToken = ref<string | null>(null)
|
||||
const user = ref<Labrinth.Users.v2.User | null>(null)
|
||||
const user = ref<AuthUser | null>(null)
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
session_token: sessionToken,
|
||||
@@ -26,7 +26,7 @@ export function setupAuthProvider(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user