refactor: removing useAsyncData for tanstack query (#5262)
* refactor: most places with useAsyncData replaced with tanstack query * refactor report list and report view * refactor organization page to use tanstack query * fix types * refactor collection page and include proper loading state * fix followed projects proper loading state * fix 404 handling * fix organization loading and 404 states * pnpm prepr * refactor: remove useAsyncData on newsletter button * refactor: remove useAsyncData on auth globals fetch * refactor: settings/billing/index.vue to useQuery instead of useAsyncData * refactor: user page to remove useAsyncData * pnpm prepr * fix reports pages * fix notifications page * fix billing page cannot read properties of null and prop warnings * fix refresh causing 404 by removing useBaseFetch and use api-client * fix stale data after removing organization from project * pnpm prepr * fix news erroring in build * fix: project page loads header only after content * fix: user page tanstack problems (start on migrating away from useBaseFetch) * fix: start swapping useBaseFetch usages to api-client * Revert "fix: start swapping useBaseFetch usages to api-client" This reverts commit 3df3fab11d535159132b1288dd7cacc38282b553. * fix: remove debug logging * fix: lint --------- Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -1,19 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, MailIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { ButtonStyled, defineMessages, injectModrinthClient, useVIntl } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const auth = await useAuth()
|
||||
const messages = defineMessages({
|
||||
tooltipSubscribe: {
|
||||
id: 'ui.newsletter-button.tooltip',
|
||||
defaultMessage: 'Subscribe to the Modrinth newsletter',
|
||||
},
|
||||
subscribe: {
|
||||
id: 'ui.newsletter-button.subscribe',
|
||||
defaultMessage: 'Subscribe',
|
||||
},
|
||||
subscribed: {
|
||||
id: 'ui.newsletter-button.subscribed',
|
||||
defaultMessage: 'Subscribed!',
|
||||
},
|
||||
})
|
||||
|
||||
const auth = (await useAuth()) as unknown as {
|
||||
value: { user: { id: string; username: string; email: string; created: string } }
|
||||
}
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const showSubscriptionConfirmation = ref(false)
|
||||
const showSubscribeButton = useAsyncData(
|
||||
async () => {
|
||||
|
||||
const { data: showSubscribeButton, isSuccess } = useQuery({
|
||||
queryKey: computed(() => ['newsletter', 'subscribed', auth.value?.user?.id]),
|
||||
queryFn: async () => {
|
||||
if (auth.value?.user) {
|
||||
try {
|
||||
const { subscribed } = await useBaseFetch('auth/email/subscribe', {
|
||||
method: 'GET',
|
||||
})
|
||||
const { subscribed } = await client.labrinth.auth_internal.getNewsletterStatus()
|
||||
return !subscribed
|
||||
} catch {
|
||||
return true
|
||||
@@ -22,36 +42,31 @@ const showSubscribeButton = useAsyncData(
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ watch: [auth], server: false },
|
||||
)
|
||||
enabled: computed(() => !!auth.value?.user),
|
||||
})
|
||||
|
||||
async function subscribe() {
|
||||
try {
|
||||
await useBaseFetch('auth/email/subscribe', {
|
||||
method: 'POST',
|
||||
})
|
||||
await client.labrinth.auth_internal.subscribeNewsletter()
|
||||
showSubscriptionConfirmation.value = true
|
||||
} catch {
|
||||
// Ignored
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
showSubscriptionConfirmation.value = false
|
||||
showSubscribeButton.status.value = 'success'
|
||||
showSubscribeButton.data.value = false
|
||||
queryClient.setQueryData(['newsletter', 'subscribed', auth.value?.user?.id], false)
|
||||
}, 2500)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonStyled
|
||||
v-if="showSubscribeButton.status.value === 'success' && showSubscribeButton.data.value"
|
||||
color="brand"
|
||||
type="outlined"
|
||||
>
|
||||
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
||||
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
||||
<template v-else> <CheckIcon /> Subscribed! </template>
|
||||
<ButtonStyled v-if="isSuccess && showSubscribeButton" color="brand" type="outlined">
|
||||
<button v-tooltip="formatMessage(messages.tooltipSubscribe)" @click="subscribe">
|
||||
<template v-if="!showSubscriptionConfirmation">
|
||||
<MailIcon /> {{ formatMessage(messages.subscribe) }}
|
||||
</template>
|
||||
<template v-else> <CheckIcon /> {{ formatMessage(messages.subscribed) }} </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
@@ -44,8 +44,11 @@
|
||||
import { MessageIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -121,10 +124,10 @@ const apiEndpoint = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const { data: limits } = await useAsyncData<UserLimits | undefined>(
|
||||
`limits-${props.type}`,
|
||||
() => useBaseFetch(apiEndpoint.value, { apiVersion: 3 }) as Promise<UserLimits>,
|
||||
)
|
||||
const { data: limits } = useQuery({
|
||||
queryKey: computed(() => ['limits', props.type]),
|
||||
queryFn: () => useBaseFetch(apiEndpoint.value, { apiVersion: 3 }) as Promise<UserLimits>,
|
||||
})
|
||||
|
||||
const typeName = computed<{ singular: string; plural: string }>(() => {
|
||||
switch (props.type) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="report">
|
||||
<div v-if="report" class="report">
|
||||
<div v-if="report.item_type === 'project'" class="item-info">
|
||||
<nuxt-link
|
||||
v-if="report.project"
|
||||
:to="`/${$getProjectTypeForUrl(report.project.project_type, report.project.loaders)}/${
|
||||
report.project.slug
|
||||
}`"
|
||||
@@ -38,27 +39,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="report.item_type === 'version'" class="item-info">
|
||||
<nuxt-link
|
||||
:to="`/project/${report.project.slug}/version/${report.version.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<div class="backed-svg" :class="{ raised: raised }">
|
||||
<VersionIcon />
|
||||
</div>
|
||||
<span class="title">{{ report.version.name }}</span>
|
||||
</nuxt-link>
|
||||
of
|
||||
<nuxt-link :to="`/project/${report.project.slug}`" class="iconified-stacked-link">
|
||||
<Avatar :src="report.project.icon_url" size="xs" no-shadow :raised="raised" />
|
||||
<div class="stacked">
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<template v-if="report.version && report.project">
|
||||
<nuxt-link
|
||||
:to="`/project/${report.project.slug}/version/${report.version.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<div class="backed-svg" :class="{ raised: raised }">
|
||||
<VersionIcon />
|
||||
</div>
|
||||
<span class="title">{{ report.version.name }}</span>
|
||||
</nuxt-link>
|
||||
of
|
||||
<nuxt-link :to="`/project/${report.project.slug}`" class="iconified-stacked-link">
|
||||
<Avatar :src="report.project.icon_url" size="xs" no-shadow :raised="raised" />
|
||||
<div class="stacked">
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="item-info">
|
||||
<div class="backed-svg" :class="{ raised: raised }">
|
||||
@@ -79,7 +82,7 @@
|
||||
:link="`/${moderation ? 'moderation' : 'dashboard'}/report/${report.id}`"
|
||||
:auth="auth"
|
||||
/>
|
||||
<div class="reporter-info">
|
||||
<div v-if="report.reporterUser" class="reporter-info">
|
||||
<ReportIcon class="inline-svg" />
|
||||
Reported by
|
||||
<span v-if="auth.user.id === report.reporterUser.id">you</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h2>Report details</h2>
|
||||
<ReportInfo :report="report" :show-thread="false" :show-message="false" :auth="auth" />
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<section v-if="report && thread" class="universal-card">
|
||||
<h2>Messages</h2>
|
||||
<ConversationThread
|
||||
:thread="thread"
|
||||
@@ -21,9 +21,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -41,74 +45,90 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const report = ref(null)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
await fetchReport().then((result) => {
|
||||
report.value = result
|
||||
// Fetch raw report
|
||||
const { data: rawReport } = useQuery({
|
||||
queryKey: computed(() => ['report', props.reportId]),
|
||||
queryFn: async () => {
|
||||
const data = await useBaseFetch(`report/${props.reportId}`)
|
||||
data.item_id = data.item_id.replace(/"/g, '')
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: rawThread } = await useAsyncData(`thread/${report.value.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${report.value.thread_id}`),
|
||||
// Compute user IDs needed
|
||||
const userIds = computed(() => {
|
||||
if (!rawReport.value) return []
|
||||
const ids = [rawReport.value.reporter]
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
ids.push(rawReport.value.item_id)
|
||||
}
|
||||
return ids
|
||||
})
|
||||
|
||||
// Fetch users
|
||||
const { data: users } = useQuery({
|
||||
queryKey: computed(() => ['users', userIds.value]),
|
||||
queryFn: () => useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds.value))}`),
|
||||
enabled: computed(() => userIds.value.length > 0),
|
||||
})
|
||||
|
||||
// Version ID if applicable
|
||||
const versionId = computed(() =>
|
||||
rawReport.value?.item_type === 'version' ? rawReport.value.item_id : null,
|
||||
)
|
||||
|
||||
// Fetch version
|
||||
const { data: version } = useQuery({
|
||||
queryKey: computed(() => ['version', versionId.value]),
|
||||
queryFn: () => useBaseFetch(`version/${versionId.value}`),
|
||||
enabled: computed(() => !!versionId.value),
|
||||
})
|
||||
|
||||
// Project ID
|
||||
const projectId = computed(() => {
|
||||
if (version.value) return version.value.project_id
|
||||
if (rawReport.value?.item_type === 'project') return rawReport.value.item_id
|
||||
return null
|
||||
})
|
||||
|
||||
// Fetch project
|
||||
const { data: project } = useQuery({
|
||||
queryKey: computed(() => ['project', projectId.value]),
|
||||
queryFn: () => useBaseFetch(`project/${projectId.value}`),
|
||||
enabled: computed(() => !!projectId.value),
|
||||
})
|
||||
|
||||
// Assemble the full report object
|
||||
const report = computed(() => {
|
||||
if (!rawReport.value) return null
|
||||
return {
|
||||
...rawReport.value,
|
||||
project: project.value ?? null,
|
||||
version: version.value ?? null,
|
||||
reporterUser: (users.value || []).find((user) => user.id === rawReport.value.reporter),
|
||||
user:
|
||||
rawReport.value.item_type === 'user'
|
||||
? (users.value || []).find((user) => user.id === rawReport.value.item_id)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch thread
|
||||
const { data: rawThread } = useQuery({
|
||||
queryKey: computed(() => ['thread', report.value?.thread_id]),
|
||||
queryFn: () => useBaseFetch(`thread/${report.value.thread_id}`),
|
||||
enabled: computed(() => !!report.value?.thread_id),
|
||||
})
|
||||
|
||||
const thread = computed(() =>
|
||||
rawThread.value && report.value ? addReportMessage(rawThread.value, report.value) : null,
|
||||
)
|
||||
const thread = computed(() => addReportMessage(rawThread.value, report.value))
|
||||
|
||||
async function updateThread(newThread) {
|
||||
rawThread.value = newThread
|
||||
report.value = await fetchReport()
|
||||
}
|
||||
|
||||
async function fetchReport() {
|
||||
const { data: rawReport } = await useAsyncData(`report/${props.reportId}`, () =>
|
||||
useBaseFetch(`report/${props.reportId}`),
|
||||
)
|
||||
rawReport.value.item_id = rawReport.value.item_id.replace(/"/g, '')
|
||||
|
||||
const userIds = []
|
||||
userIds.push(rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
userIds.push(rawReport.value.item_id)
|
||||
}
|
||||
|
||||
const versionId = rawReport.value.item_type === 'version' ? rawReport.value.item_id : null
|
||||
|
||||
let users = []
|
||||
if (userIds.length > 0) {
|
||||
const { data: usersVal } = await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||
)
|
||||
users = usersVal.value
|
||||
}
|
||||
|
||||
let version = null
|
||||
if (versionId) {
|
||||
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
|
||||
useBaseFetch(`version/${versionId}`),
|
||||
)
|
||||
version = versionVal.value
|
||||
}
|
||||
|
||||
const projectId = version
|
||||
? version.project_id
|
||||
: rawReport.value.item_type === 'project'
|
||||
? rawReport.value.item_id
|
||||
: null
|
||||
|
||||
let project = null
|
||||
if (projectId) {
|
||||
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
|
||||
useBaseFetch(`project/${projectId}`),
|
||||
)
|
||||
project = projectVal.value
|
||||
}
|
||||
|
||||
const reportData = rawReport.value
|
||||
reportData.project = project
|
||||
reportData.version = version
|
||||
reportData.reporterUser = users.find((user) => user.id === rawReport.value.reporter)
|
||||
if (rawReport.value.item_type === 'user') {
|
||||
reportData.user = users.find((user) => user.id === rawReport.value.item_id)
|
||||
}
|
||||
return reportData
|
||||
queryClient.setQueryData(['thread', report.value?.thread_id], newThread)
|
||||
await queryClient.invalidateQueries({ queryKey: ['report', props.reportId] })
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -21,12 +21,15 @@
|
||||
:auth="auth"
|
||||
class="universal-card recessed"
|
||||
/>
|
||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||
<p v-if="filteredReports.length === 0">You don't have any active reports.</p>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Chips } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ReportInfo from '~/components/ui/report/ReportInfo.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
import { addReportMessage } from '~/helpers/threads.js'
|
||||
import { asEncodedJsonArray, fetchSegmented } from '~/utils/fetch-helpers.ts'
|
||||
|
||||
@@ -43,77 +46,113 @@ const props = defineProps({
|
||||
|
||||
const viewMode = ref('open')
|
||||
const reasonFilter = ref('All')
|
||||
const reports = ref([])
|
||||
|
||||
const MAX_REPORTS = 1500
|
||||
|
||||
let { data: rawReports } = await useAsyncData('report', () =>
|
||||
useBaseFetch(`report?count=${MAX_REPORTS}`),
|
||||
)
|
||||
|
||||
rawReports = rawReports.value.map((report) => {
|
||||
report.item_id = report.item_id.replace(/"/g, '')
|
||||
return report
|
||||
const { data: rawReportsData } = useQuery({
|
||||
queryKey: ['reports', MAX_REPORTS],
|
||||
queryFn: () => useBaseFetch(`report?count=${MAX_REPORTS}`),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const reporterUsers = rawReports.map((report) => report.reporter)
|
||||
const reportedUsers = rawReports
|
||||
.filter((report) => report.item_type === 'user')
|
||||
.map((report) => report.item_id)
|
||||
const versionReports = rawReports.filter((report) => report.item_type === 'version')
|
||||
const versionIds = [...new Set(versionReports.map((report) => report.item_id))]
|
||||
const userIds = [...new Set(reporterUsers.concat(reportedUsers))]
|
||||
const threadIds = [
|
||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
||||
]
|
||||
const reasons = ['All', ...new Set(rawReports.map((report) => report.report_type))]
|
||||
const rawReports = computed(() =>
|
||||
rawReportsData.value.map((report) => ({
|
||||
...report,
|
||||
item_id: report.item_id.replace(/"/g, ''),
|
||||
})),
|
||||
)
|
||||
|
||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||
fetchSegmented(userIds, (ids) => `users?ids=${asEncodedJsonArray(ids)}`),
|
||||
),
|
||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||
fetchSegmented(versionIds, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
||||
),
|
||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||
const reporterUsers = computed(() => rawReports.value.map((report) => report.reporter))
|
||||
const reportedUsers = computed(() =>
|
||||
rawReports.value.filter((report) => report.item_type === 'user').map((report) => report.item_id),
|
||||
)
|
||||
const versionReports = computed(() =>
|
||||
rawReports.value.filter((report) => report.item_type === 'version'),
|
||||
)
|
||||
const versionIds = computed(() => [
|
||||
...new Set(versionReports.value.map((report) => report.item_id)),
|
||||
])
|
||||
const userIds = computed(() => [...new Set(reporterUsers.value.concat(reportedUsers.value))])
|
||||
const threadIds = computed(() => [
|
||||
...new Set(
|
||||
rawReports.value.filter((report) => report.thread_id).map((report) => report.thread_id),
|
||||
),
|
||||
])
|
||||
const reasons = computed(() => [
|
||||
'All',
|
||||
...new Set(rawReports.value.map((report) => report.report_type)),
|
||||
])
|
||||
|
||||
const reportedProjects = rawReports
|
||||
.filter((report) => report.item_type === 'project')
|
||||
.map((report) => report.item_id)
|
||||
const versionProjects = versions.value.map((version) => version.project_id)
|
||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))]
|
||||
|
||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||
)
|
||||
|
||||
reports.value = rawReports.map((report) => {
|
||||
report.reporterUser = users.value.find((user) => user.id === report.reporter)
|
||||
if (report.item_type === 'user') {
|
||||
report.user = users.value.find((user) => user.id === report.item_id)
|
||||
} else if (report.item_type === 'project') {
|
||||
report.project = projects.value.find((project) => project.id === report.item_id)
|
||||
} else if (report.item_type === 'version') {
|
||||
report.version = versions.value.find((version) => version.id === report.item_id)
|
||||
report.project = projects.value.find((project) => project.id === report.version.project_id)
|
||||
}
|
||||
if (report.thread_id) {
|
||||
report.thread = addReportMessage(
|
||||
threads.value.find((thread) => report.thread_id === thread.id),
|
||||
report,
|
||||
)
|
||||
}
|
||||
report.open = true
|
||||
return report
|
||||
const { data: users } = useQuery({
|
||||
queryKey: computed(() => ['users', userIds.value]),
|
||||
queryFn: () => fetchSegmented(userIds.value, (ids) => `users?ids=${asEncodedJsonArray(ids)}`),
|
||||
enabled: computed(() => userIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const { data: versions } = useQuery({
|
||||
queryKey: computed(() => ['versions', versionIds.value]),
|
||||
queryFn: () =>
|
||||
fetchSegmented(versionIds.value, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
||||
enabled: computed(() => versionIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const { data: threads } = useQuery({
|
||||
queryKey: computed(() => ['threads', threadIds.value]),
|
||||
queryFn: () => fetchSegmented(threadIds.value, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||
enabled: computed(() => threadIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const reportedProjects = computed(() =>
|
||||
rawReports.value
|
||||
.filter((report) => report.item_type === 'project')
|
||||
.map((report) => report.item_id),
|
||||
)
|
||||
const versionProjects = computed(() => versions.value.map((version) => version.project_id))
|
||||
const projectIds = computed(() => [
|
||||
...new Set(reportedProjects.value.concat(versionProjects.value)),
|
||||
])
|
||||
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: computed(() => ['projects', projectIds.value]),
|
||||
queryFn: () =>
|
||||
fetchSegmented(projectIds.value, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||
enabled: computed(() => projectIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const userMap = computed(() => new Map(users.value.map((u) => [u.id, u])))
|
||||
const versionMap = computed(() => new Map(versions.value.map((v) => [v.id, v])))
|
||||
const projectMap = computed(() => new Map(projects.value.map((p) => [p.id, p])))
|
||||
const threadMap = computed(() => new Map(threads.value.map((t) => [t.id, t])))
|
||||
|
||||
const reports = computed(() =>
|
||||
rawReports.value.map((report) => {
|
||||
const enrichedReport = { ...report }
|
||||
enrichedReport.reporterUser = userMap.value.get(report.reporter)
|
||||
if (report.item_type === 'user') {
|
||||
enrichedReport.user = userMap.value.get(report.item_id)
|
||||
} else if (report.item_type === 'project') {
|
||||
enrichedReport.project = projectMap.value.get(report.item_id)
|
||||
} else if (report.item_type === 'version') {
|
||||
enrichedReport.version = versionMap.value.get(report.item_id)
|
||||
enrichedReport.project = projectMap.value.get(enrichedReport.version?.project_id)
|
||||
}
|
||||
if (report.thread_id) {
|
||||
const thread = threadMap.value.get(report.thread_id)
|
||||
enrichedReport.thread = thread ? addReportMessage(thread, enrichedReport) : null
|
||||
}
|
||||
enrichedReport.open = true
|
||||
return enrichedReport
|
||||
}),
|
||||
)
|
||||
|
||||
const filteredReports = computed(() =>
|
||||
reports.value?.filter(
|
||||
(x) =>
|
||||
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
||||
(props.moderation || x.reporterUser?.id === props.auth.user.id) &&
|
||||
(viewMode.value === 'open' ? x.open : !x.open) &&
|
||||
(reasonFilter.value === 'All' || reasonFilter.value === x.report_type),
|
||||
),
|
||||
|
||||
@@ -133,13 +133,13 @@ export function groupNotifications(notifications: PlatformNotification[]): Platf
|
||||
const current = notifications[i]
|
||||
const next = notifications[i + 1]
|
||||
if (current.body && i < notifications.length - 1 && isSimilar(current, next)) {
|
||||
current.grouped_notifs = [next]
|
||||
const groupedNotif = { ...current, grouped_notifs: [next] }
|
||||
let j = i + 2
|
||||
while (j < notifications.length && isSimilar(current, notifications[j])) {
|
||||
current.grouped_notifs.push(notifications[j])
|
||||
groupedNotif.grouped_notifs.push(notifications[j])
|
||||
j++
|
||||
}
|
||||
grouped.push(current)
|
||||
grouped.push(groupedNotif)
|
||||
i = j - 1
|
||||
} else {
|
||||
grouped.push(current)
|
||||
|
||||
@@ -2,17 +2,31 @@ export function addReportMessage(thread, report) {
|
||||
if (!thread || !report) {
|
||||
return thread
|
||||
}
|
||||
if (
|
||||
!thread.members.some((user) => {
|
||||
return user.id === report.reporterUser.id
|
||||
})
|
||||
) {
|
||||
thread.members.push(report.reporterUser)
|
||||
|
||||
const reporterId = report.reporterUser?.id ?? report.reporter
|
||||
if (!reporterId) {
|
||||
return thread
|
||||
}
|
||||
if (!thread.messages.some((message) => message.id === 'original')) {
|
||||
thread.messages.push({
|
||||
|
||||
const members = Array.isArray(thread.members) ? [...thread.members] : []
|
||||
const messages = Array.isArray(thread.messages) ? [...thread.messages] : []
|
||||
|
||||
let changed = false
|
||||
|
||||
if (
|
||||
!members.some((user) => {
|
||||
return user?.id === reporterId
|
||||
}) &&
|
||||
report.reporterUser
|
||||
) {
|
||||
members.push(report.reporterUser)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (!messages.some((message) => message?.id === 'original')) {
|
||||
messages.push({
|
||||
id: 'original',
|
||||
author_id: report.reporterUser.id,
|
||||
author_id: reporterId,
|
||||
body: {
|
||||
type: 'text',
|
||||
body: report.body,
|
||||
@@ -21,6 +35,16 @@ export function addReportMessage(thread, report) {
|
||||
},
|
||||
created: report.created,
|
||||
})
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return thread
|
||||
}
|
||||
|
||||
return {
|
||||
...thread,
|
||||
members,
|
||||
messages,
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
||||
@@ -735,10 +735,12 @@ import {
|
||||
commonMessages,
|
||||
commonProjectTypeCategoryMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
OverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import { getTaxThreshold } from '@/providers/creator-withdraw.ts'
|
||||
import TextLogo from '~/components/brand/TextLogo.vue'
|
||||
@@ -775,10 +777,12 @@ const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
const router = useNativeRouter()
|
||||
const link = config.public.siteUrl + route.path.replace(/\/+$/, '')
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const { data: payoutBalance } = await useAsyncData('payout/balance', () => {
|
||||
if (!auth.value.user) return null
|
||||
return useBaseFetch('payout/balance', { apiVersion: 3 })
|
||||
const { data: payoutBalance } = useQuery({
|
||||
queryKey: ['payout', 'balance'],
|
||||
queryFn: () => client.labrinth.payout_v3.getBalance(),
|
||||
enabled: computed(() => !!auth.value.user),
|
||||
})
|
||||
|
||||
const showTaxComplianceBanner = computed(() => {
|
||||
@@ -1140,7 +1144,7 @@ async function onKeyDown(event) {
|
||||
rCount.value++
|
||||
|
||||
if (randomProjects.value.length < 3) {
|
||||
randomProjects.value = await useBaseFetch('projects_random?count=50').catch((err) => {
|
||||
randomProjects.value = await client.labrinth.projects_v2.getRandom(50).catch((err) => {
|
||||
console.error(err)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -3197,6 +3197,15 @@
|
||||
"ui.latest-news-row.view-all": {
|
||||
"message": "View all news"
|
||||
},
|
||||
"ui.newsletter-button.subscribe": {
|
||||
"message": "Subscribe"
|
||||
},
|
||||
"ui.newsletter-button.subscribed": {
|
||||
"message": "Subscribed!"
|
||||
},
|
||||
"ui.newsletter-button.tooltip": {
|
||||
"message": "Subscribe to the Modrinth newsletter"
|
||||
},
|
||||
"version.environment.none.description": {
|
||||
"message": "The environment for this version has not been specified."
|
||||
},
|
||||
|
||||
31
apps/frontend/src/middleware/user.global.ts
Normal file
31
apps/frontend/src/middleware/user.global.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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) {
|
||||
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),
|
||||
})
|
||||
|
||||
if (!user) return
|
||||
|
||||
if (user.username !== userId) {
|
||||
return navigateTo(`/user/${user.username}`, {
|
||||
redirectCode: 301,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Let the page handle 404s and other errors
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<template v-if="project">
|
||||
<template v-if="project && projectV3Loaded">
|
||||
<Teleport v-if="flags.projectBackground" to="#fixed-background-teleport">
|
||||
<ProjectBackgroundGradient :project="project" />
|
||||
</Teleport>
|
||||
@@ -1672,10 +1672,9 @@ const {
|
||||
error: _projectV3Error,
|
||||
isPending: projectV3Pending,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['project', 'v3', projectId.value]),
|
||||
queryFn: () => client.labrinth.projects_v3.get(projectId.value),
|
||||
queryKey: computed(() => ['project', 'v3', routeProjectId.value]),
|
||||
queryFn: () => client.labrinth.projects_v3.get(routeProjectId.value),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: computed(() => !!projectId.value),
|
||||
})
|
||||
|
||||
// Server sidebar: modpack version + project for required content
|
||||
@@ -1811,13 +1810,17 @@ const {
|
||||
|
||||
// Organization
|
||||
// Only fetch organization if project belongs to one
|
||||
const { data: organization } = useQuery({
|
||||
const { data: organizationRaw } = useQuery({
|
||||
queryKey: computed(() => ['project', projectId.value, 'organization']),
|
||||
queryFn: () => client.labrinth.projects_v3.getOrganization(projectId.value),
|
||||
staleTime: STALE_TIME,
|
||||
enabled: computed(() => !!projectId.value && !!projectRaw.value?.organization),
|
||||
})
|
||||
|
||||
// When project is removed from org, enabled becomes false but TanStack keeps stale data.
|
||||
// Return null when the project no longer belongs to an organization.
|
||||
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
|
||||
|
||||
// Transform versionsV3 to be same shape as versionsV2 for compatibility in project pages
|
||||
const versionsRaw = computed(() => {
|
||||
return (versionsV3.value ?? []).map((v) => {
|
||||
|
||||
@@ -101,8 +101,11 @@
|
||||
<script setup>
|
||||
import { CheckIcon, IssuesIcon, XIcon } from '@modrinth/assets'
|
||||
import { Badge, injectNotificationManager, injectProjectPageContext } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
import {
|
||||
getProjectLink,
|
||||
isApproved,
|
||||
@@ -116,11 +119,13 @@ const { addNotification } = injectNotificationManager()
|
||||
const { projectV2: project, currentMember, invalidate } = injectProjectPageContext()
|
||||
|
||||
const auth = await useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: thread } = await useAsyncData(
|
||||
() => `thread/${project.value.thread_id}`,
|
||||
() => useBaseFetch(`thread/${project.value.thread_id}`),
|
||||
)
|
||||
const { data: thread } = useQuery({
|
||||
queryKey: computed(() => ['thread', project.value?.thread_id]),
|
||||
queryFn: () => useBaseFetch(`thread/${project.value.thread_id}`),
|
||||
enabled: computed(() => !!project.value?.thread_id),
|
||||
})
|
||||
|
||||
async function setStatus(status) {
|
||||
startLoading()
|
||||
@@ -135,7 +140,7 @@ async function setStatus(status) {
|
||||
|
||||
project.value.status = status
|
||||
await invalidate()
|
||||
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
|
||||
await queryClient.invalidateQueries({ queryKey: ['thread', project.value?.thread_id] })
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
|
||||
@@ -563,8 +563,10 @@ import {
|
||||
StyledInput,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import ConfirmTransferProjectModal from '~/components/ui/ConfirmTransferProjectModal.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
import { removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -619,10 +621,13 @@ const selectedOrganizationId = ref('')
|
||||
const transferData = ref(null)
|
||||
const transferModal = ref(null)
|
||||
|
||||
const { data: organizations } = useAsyncData('organizations', () => {
|
||||
return useBaseFetch('user/' + auth.value?.user.id + '/organizations', {
|
||||
apiVersion: 3,
|
||||
})
|
||||
const { data: organizations } = useQuery({
|
||||
queryKey: computed(() => ['user', auth.value?.user?.id, 'organizations']),
|
||||
queryFn: () =>
|
||||
useBaseFetch('user/' + auth.value?.user.id + '/organizations', {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
enabled: computed(() => !!auth.value?.user?.id),
|
||||
})
|
||||
|
||||
const organizationOptions = computed(() =>
|
||||
|
||||
@@ -95,6 +95,10 @@ import {
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import type { AffiliateLink, User } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
@@ -109,11 +113,12 @@ const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')
|
||||
const {
|
||||
data: affiliateCodes,
|
||||
error,
|
||||
refresh,
|
||||
} = await useAsyncData(
|
||||
'AffiliateLinks',
|
||||
() => useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
|
||||
)
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['affiliate'],
|
||||
queryFn: () =>
|
||||
useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
|
||||
})
|
||||
|
||||
const filterQuery = ref('')
|
||||
const creatingLink = ref(false)
|
||||
@@ -130,16 +135,13 @@ const userIds = computed(() => {
|
||||
return Array.from(ids)
|
||||
})
|
||||
|
||||
const { data: users } = await useAsyncData(
|
||||
'admin-affiliates-bulk-users',
|
||||
() => {
|
||||
const { data: users } = useQuery({
|
||||
queryKey: computed(() => ['users-bulk', userIds.value]),
|
||||
queryFn: () => {
|
||||
if (userIds.value.length === 0) return Promise.resolve([])
|
||||
return useBaseFetch(`users?ids=${JSON.stringify(userIds.value)}`) as Promise<User[]>
|
||||
},
|
||||
{
|
||||
watch: [userIds],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const userMap = computed(() => {
|
||||
if (!users.value) {
|
||||
@@ -225,7 +227,7 @@ async function createAffiliateCode(data: { sourceName: string; username?: string
|
||||
internal: true,
|
||||
})
|
||||
|
||||
await refresh()
|
||||
await refetch()
|
||||
createModal.value?.close()
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
@@ -255,7 +257,7 @@ async function confirmRevokeAffiliateCode() {
|
||||
internal: true,
|
||||
})
|
||||
|
||||
await refresh()
|
||||
await refetch()
|
||||
revokeModal.value?.hide()
|
||||
revokingAffiliateUsername.value = null
|
||||
revokingAffiliateId.value = null
|
||||
|
||||
@@ -339,9 +339,11 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const formatPrice = useFormatPrice()
|
||||
@@ -370,9 +372,10 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const { data: user } = await useAsyncData(`user/${route.params.id}`, () =>
|
||||
useBaseFetch(`user/${route.params.id}`),
|
||||
)
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['user', route.params.id],
|
||||
queryFn: () => useBaseFetch(`user/${route.params.id}`),
|
||||
})
|
||||
|
||||
if (!user.value) {
|
||||
throw createError({
|
||||
@@ -382,27 +385,25 @@ if (!user.value) {
|
||||
})
|
||||
}
|
||||
|
||||
let subscriptions, charges, refreshCharges
|
||||
try {
|
||||
;[{ data: subscriptions }, { data: charges, refresh: refreshCharges }] = await Promise.all([
|
||||
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
|
||||
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
),
|
||||
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
|
||||
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
),
|
||||
])
|
||||
} catch {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
})
|
||||
}
|
||||
const { data: subscriptions } = useQuery({
|
||||
queryKey: computed(() => ['billing', 'subscriptions', user.value?.id]),
|
||||
queryFn: () =>
|
||||
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
enabled: computed(() => !!user.value?.id),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const { data: charges, refetch: refreshCharges } = useQuery({
|
||||
queryKey: computed(() => ['billing', 'payments', user.value?.id]),
|
||||
queryFn: () =>
|
||||
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
enabled: computed(() => !!user.value?.id),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const subscriptionCharges = computed(() => {
|
||||
return subscriptions.value.map((subscription) => {
|
||||
|
||||
@@ -91,6 +91,8 @@ import {
|
||||
normalizeChildren,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useAuth } from '@/composables/auth.js'
|
||||
import { useScopes } from '@/composables/auth/scopes.ts'
|
||||
@@ -163,24 +165,36 @@ const getFlowIdAuthorization = async () => {
|
||||
|
||||
const {
|
||||
data: authorizationData,
|
||||
pending,
|
||||
isPending: pending,
|
||||
error,
|
||||
} = await useAsyncData('authorization', getFlowIdAuthorization)
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['authorization', clientId, redirectUri, scope, state]),
|
||||
queryFn: getFlowIdAuthorization,
|
||||
enabled: computed(() => !!clientId && !!redirectUri && !!scope),
|
||||
})
|
||||
|
||||
const { data: app } = await useAsyncData('oauth/app/' + clientId, () =>
|
||||
useBaseFetch('oauth/app/' + clientId, {
|
||||
method: 'GET',
|
||||
internal: true,
|
||||
}),
|
||||
)
|
||||
const { data: app } = useQuery({
|
||||
queryKey: computed(() => ['oauth/app', clientId]),
|
||||
queryFn: () =>
|
||||
useBaseFetch('oauth/app/' + clientId, {
|
||||
method: 'GET',
|
||||
internal: true,
|
||||
}),
|
||||
enabled: computed(() => !!clientId),
|
||||
})
|
||||
|
||||
const scopeDefinitions = scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0))
|
||||
const { data: createdBy } = useQuery({
|
||||
queryKey: computed(() => ['user', app.value?.created_by]),
|
||||
queryFn: () =>
|
||||
useBaseFetch('user/' + app.value.created_by, {
|
||||
method: 'GET',
|
||||
apiVersion: 3,
|
||||
}),
|
||||
enabled: computed(() => !!app.value?.created_by),
|
||||
})
|
||||
|
||||
const { data: createdBy } = await useAsyncData('user/' + app.value.created_by, () =>
|
||||
useBaseFetch('user/' + app.value.created_by, {
|
||||
method: 'GET',
|
||||
apiVersion: 3,
|
||||
}),
|
||||
const scopeDefinitions = computed(() =>
|
||||
scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0)),
|
||||
)
|
||||
|
||||
const onAuthorize = async () => {
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
|
||||
@@ -162,13 +163,16 @@ if (route.query.flow) {
|
||||
|
||||
const captcha = ref()
|
||||
|
||||
const { data: globals } = await useAsyncData('auth-globals', async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
|
||||
@@ -144,7 +144,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl, getLauncherRedirectUrl } from '@/composables/auth.js'
|
||||
@@ -207,13 +207,16 @@ if (auth.value.user) {
|
||||
|
||||
const captcha = ref()
|
||||
|
||||
const { data: globals } = await useAsyncData('auth-globals', async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
|
||||
@@ -146,6 +146,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.js'
|
||||
@@ -200,13 +201,16 @@ if (auth.value.user) {
|
||||
|
||||
const captcha = ref()
|
||||
|
||||
const { data: globals } = await useAsyncData('auth-globals', async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await useBaseFetch('globals', { internal: true })
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { AffiliateLink } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
const createModal = useTemplateRef<typeof AffiliateLinkCreateModal>('createModal')
|
||||
const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')
|
||||
@@ -83,11 +84,12 @@ const { formatMessage } = useVIntl()
|
||||
const {
|
||||
data: affiliateLinks,
|
||||
error,
|
||||
refresh,
|
||||
} = await useAsyncData(
|
||||
'affiliateLinks',
|
||||
() => useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
|
||||
)
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['affiliate'],
|
||||
queryFn: () =>
|
||||
useBaseFetch('affiliate', { method: 'GET', internal: true }) as Promise<AffiliateLink[]>,
|
||||
})
|
||||
|
||||
const filterQuery = ref('')
|
||||
const creatingLink = ref(false)
|
||||
@@ -116,7 +118,7 @@ async function createAffiliateCode(data: { sourceName: string }) {
|
||||
internal: true,
|
||||
})
|
||||
|
||||
await refresh()
|
||||
await refetch()
|
||||
createModal.value?.close()
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
@@ -145,7 +147,7 @@ async function confirmRevokeAffiliateLink() {
|
||||
internal: true,
|
||||
})
|
||||
|
||||
await refresh()
|
||||
await refetch()
|
||||
revokeModal.value?.hide()
|
||||
revokingTitle.value = null
|
||||
revokingId.value = null
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
||||
|
||||
definePageMeta({
|
||||
@@ -18,7 +20,9 @@ useHead({
|
||||
const auth = await useAuth()
|
||||
const id = auth.value?.user?.id
|
||||
|
||||
const { data: projects } = await useAsyncData(`user/${id}/projects`, () =>
|
||||
useBaseFetch(`user/${id}/projects`),
|
||||
)
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: computed(() => ['user', id, 'projects']),
|
||||
queryFn: () => useBaseFetch(`user/${id}/projects`),
|
||||
enabled: computed(() => !!id),
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -159,8 +159,10 @@ import {
|
||||
useCompactNumber,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||
import { useBaseFetch } from '~/composables/fetch.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
|
||||
@@ -221,9 +223,10 @@ if (import.meta.client) {
|
||||
|
||||
const filterQuery = ref('')
|
||||
|
||||
const { data: collections } = await useAsyncData(`user/${auth.value.user.id}/collections`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
|
||||
)
|
||||
const { data: collections } = useQuery({
|
||||
queryKey: ['user', auth.value.user.id, 'collections'],
|
||||
queryFn: () => useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
|
||||
})
|
||||
|
||||
const route = useNativeRoute()
|
||||
const router = useNativeRouter()
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
:auth="auth"
|
||||
raised
|
||||
compact
|
||||
@update:notifications="() => refresh()"
|
||||
@update:notifications="() => refetch()"
|
||||
/>
|
||||
<nuxt-link
|
||||
v-if="extraNotifs > 0"
|
||||
@@ -98,6 +98,7 @@
|
||||
<script setup>
|
||||
import { ChevronRightIcon, HistoryIcon } from '@modrinth/assets'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import NotificationItem from '~/components/ui/NotificationItem.vue'
|
||||
import { fetchExtraNotificationData, groupNotifications } from '~/helpers/platform-notifications.ts'
|
||||
@@ -108,11 +109,11 @@ useHead({
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const [{ data: projects }] = await Promise.all([
|
||||
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`),
|
||||
),
|
||||
])
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: computed(() => ['user', auth.value?.user?.id, 'projects']),
|
||||
queryFn: async () => await useBaseFetch(`user/${auth.value?.user?.id}/projects`),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const downloadsProjectCount = computed(
|
||||
() => projects.value.filter((project) => project.downloads > 0).length,
|
||||
@@ -121,23 +122,24 @@ const followersProjectCount = computed(
|
||||
() => projects.value.filter((project) => project.followers > 0).length,
|
||||
)
|
||||
|
||||
const { data, refresh } = await useAsyncData(async () => {
|
||||
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: computed(() => ['user', auth.value?.user?.id, 'notifications']),
|
||||
queryFn: async () => {
|
||||
const notifications = await useBaseFetch(`user/${auth.value?.user?.id}/notifications`)
|
||||
|
||||
const filteredNotifications = notifications.filter((notif) => !notif.read)
|
||||
const slice = filteredNotifications.slice(0, 30) // send first 30 notifs to be grouped before trimming to 3
|
||||
const filteredNotifications = notifications.filter((notif) => !notif.read)
|
||||
const slice = filteredNotifications.slice(0, 30)
|
||||
|
||||
return fetchExtraNotificationData(slice).then((notifications) => {
|
||||
notifications = groupNotifications(notifications).slice(0, 3)
|
||||
return { notifications, extraNotifs: filteredNotifications.length - slice.length }
|
||||
})
|
||||
return fetchExtraNotificationData(slice).then((notifications) => {
|
||||
notifications = groupNotifications(notifications).slice(0, 3)
|
||||
return { notifications, extraNotifs: filteredNotifications.length - slice.length }
|
||||
})
|
||||
},
|
||||
enabled: computed(() => !!auth.value?.user?.id),
|
||||
})
|
||||
|
||||
const notifications = computed(() => {
|
||||
if (data.value === null) {
|
||||
return []
|
||||
}
|
||||
return data.value.notifications
|
||||
return data.value?.notifications ?? []
|
||||
})
|
||||
|
||||
const extraNotifs = computed(() => (data.value ? data.value.extraNotifs : 0))
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
:format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
|
||||
:capitalize="false"
|
||||
/>
|
||||
<p v-if="pending">Loading notifications...</p>
|
||||
<p v-if="isPending">Loading notifications...</p>
|
||||
<template v-else-if="error">
|
||||
<p>Error loading notifications:</p>
|
||||
<pre>
|
||||
@@ -45,7 +45,7 @@
|
||||
:notification="notification"
|
||||
:auth="auth"
|
||||
raised
|
||||
@update:notifications="() => refresh()"
|
||||
@update:notifications="() => refetch()"
|
||||
/>
|
||||
</template>
|
||||
<p v-else>You don't have any unread notifications.</p>
|
||||
@@ -59,6 +59,7 @@
|
||||
import { CheckCheckIcon, HistoryIcon } from '@modrinth/assets'
|
||||
import { Button, Chips, Pagination } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import NotificationItem from '~/components/ui/NotificationItem.vue'
|
||||
@@ -81,11 +82,19 @@ const selectedType = ref('all')
|
||||
const page = ref(1)
|
||||
const perPage = ref(50)
|
||||
|
||||
const { data, pending, error, refresh } = await useAsyncData(
|
||||
async () => {
|
||||
const { data, isPending, error, refetch } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'user',
|
||||
auth.value?.user?.id,
|
||||
'notifications',
|
||||
page.value,
|
||||
history.value,
|
||||
selectedType.value,
|
||||
]),
|
||||
queryFn: async () => {
|
||||
const pageNum = page.value - 1
|
||||
const showRead = history.value
|
||||
const notifications = await useBaseFetch(`user/${auth.value.user.id}/notifications`)
|
||||
const notifications = await useBaseFetch(`user/${auth.value?.user?.id}/notifications`)
|
||||
|
||||
const typesInFeed = [
|
||||
...new Set(notifications.filter((n) => showRead || !n.read).map((n) => n.type)),
|
||||
@@ -107,8 +116,9 @@ const { data, pending, error, refresh } = await useAsyncData(
|
||||
hasRead: notifications.some((n) => n.read),
|
||||
}))
|
||||
},
|
||||
{ watch: [page, history, selectedType] },
|
||||
)
|
||||
enabled: computed(() => !!auth.value?.user?.id),
|
||||
placeholderData: { notifications: [], notifTypes: [], pages: 1, hasRead: false },
|
||||
})
|
||||
|
||||
const notifications = computed(() =>
|
||||
data.value ? groupNotifications(data.value.notifications, history.value) : [],
|
||||
@@ -130,7 +140,7 @@ async function readAll() {
|
||||
])
|
||||
|
||||
await markAsRead(ids)
|
||||
await refresh()
|
||||
await refetch()
|
||||
}
|
||||
|
||||
function changePage(newPage) {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<script setup>
|
||||
import { PlusIcon, UsersIcon } from '@modrinth/assets'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import { useAuth } from '~/composables/auth.js'
|
||||
@@ -60,12 +61,13 @@ const createOrgModal = ref(null)
|
||||
const auth = await useAuth()
|
||||
const uid = computed(() => auth.value.user?.id || null)
|
||||
|
||||
const { data: orgs, error } = useAsyncData('organizations', () => {
|
||||
if (!uid.value) return Promise.resolve(null)
|
||||
|
||||
return useBaseFetch('user/' + uid.value + '/organizations', {
|
||||
apiVersion: 3,
|
||||
})
|
||||
const { data: orgs, error } = useQuery({
|
||||
queryKey: computed(() => ['user', uid.value, 'organizations']),
|
||||
queryFn: () =>
|
||||
useBaseFetch('user/' + uid.value + '/organizations', {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
enabled: computed(() => !!uid.value),
|
||||
})
|
||||
|
||||
const sortedOrgs = computed(() =>
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowUpRightIcon, InProgressIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useFormatDateTime, useFormatMoney, useVIntl } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
|
||||
@@ -356,9 +357,9 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const { data: userBalance, refresh: refreshUserBalance } = await useAsyncData(
|
||||
`payout/balance`,
|
||||
async () => {
|
||||
const { data: userBalance, refetch: refreshUserBalance } = useQuery({
|
||||
queryKey: ['payout', 'balance'],
|
||||
queryFn: async () => {
|
||||
const response = (await useBaseFetch(`payout/balance`, {
|
||||
apiVersion: 3,
|
||||
})) as UserBalanceResponse
|
||||
@@ -370,28 +371,33 @@ const { data: userBalance, refresh: refreshUserBalance } = await useAsyncData(
|
||||
pending: Number(response.pending),
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const { data: payouts, refresh: refreshPayouts } = await useAsyncData(`payout/history`, () =>
|
||||
useBaseFetch(`payout/history`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
)
|
||||
const { data: payouts, refetch: refreshPayouts } = useQuery({
|
||||
queryKey: ['payout', 'history'],
|
||||
queryFn: () =>
|
||||
useBaseFetch(`payout/history`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
})
|
||||
|
||||
const userCountry = useUserCountry()
|
||||
const { data: preloadedPaymentMethods } = await useAsyncData(`payout/methods-preload`, async () => {
|
||||
const defaultCountry = userCountry.value || 'US'
|
||||
try {
|
||||
return {
|
||||
country: defaultCountry,
|
||||
methods: (await useBaseFetch('payout/methods', {
|
||||
apiVersion: 3,
|
||||
query: { country: defaultCountry },
|
||||
})) as PayoutMethod[],
|
||||
const { data: preloadedPaymentMethods } = useQuery({
|
||||
queryKey: computed(() => ['payout', 'methods-preload', userCountry.value]),
|
||||
queryFn: async () => {
|
||||
const defaultCountry = userCountry.value || 'US'
|
||||
try {
|
||||
return {
|
||||
country: defaultCountry,
|
||||
methods: (await useBaseFetch('payout/methods', {
|
||||
apiVersion: 3,
|
||||
query: { country: defaultCountry },
|
||||
})) as PayoutMethod[],
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const sortedPayouts = computed(() => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id || transaction.created"
|
||||
:transaction="transaction"
|
||||
@cancelled="refresh"
|
||||
@cancelled="refetch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'
|
||||
@@ -116,11 +117,13 @@ useHead({
|
||||
title: 'Transaction history - Modrinth',
|
||||
})
|
||||
|
||||
const { data: transactions, refresh } = await useAsyncData(`payout-history`, () =>
|
||||
useBaseFetch(`payout/history`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
)
|
||||
const { data: transactions, refetch } = useQuery({
|
||||
queryKey: ['payout', 'history'],
|
||||
queryFn: () =>
|
||||
useBaseFetch(`payout/history`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
})
|
||||
|
||||
const allTransactions = computed(() => {
|
||||
if (!transactions.value) return []
|
||||
|
||||
@@ -648,6 +648,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '@/composables/fetch.js'
|
||||
@@ -1011,14 +1012,18 @@ const selectedCurrency = ref('USD')
|
||||
const loggedOut = computed(() => !auth.value.user)
|
||||
const outOfStockUrl = 'https://discord.modrinth.com'
|
||||
|
||||
const { data: hasServers } = await useAsyncData('ServerListCountCheck', async () => {
|
||||
try {
|
||||
if (!auth.value.user) return false
|
||||
const response = await useServersFetch('servers')
|
||||
return response.servers && response.servers.length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const { data: hasServers } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'list-count', auth.value?.user?.id]),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (!auth.value.user) return false
|
||||
const response = await useServersFetch('servers')
|
||||
return response.servers && response.servers.length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
enabled: computed(() => !!auth.value?.user),
|
||||
})
|
||||
|
||||
function fetchStock(region, request) {
|
||||
@@ -1080,15 +1085,12 @@ async function fetchCapacityStatuses(customProduct = null) {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
|
||||
'ServerCapacityAll',
|
||||
fetchCapacityStatuses,
|
||||
{
|
||||
getCachedData() {
|
||||
return null // Dont cache stock data.
|
||||
},
|
||||
},
|
||||
)
|
||||
const { data: capacityStatuses, refetch: refreshCapacity } = useQuery({
|
||||
queryKey: ['server', 'capacity', 'all'],
|
||||
queryFn: fetchCapacityStatuses,
|
||||
staleTime: 0, // Dont cache stock data
|
||||
gcTime: 0,
|
||||
})
|
||||
|
||||
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0)
|
||||
const isMediumAtCapacity = computed(() => capacityStatuses.value?.medium?.available === 0)
|
||||
|
||||
@@ -118,7 +118,7 @@ import {
|
||||
StyledInput,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { StyledInput, useFormatDateTime, useFormatMoney } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -188,11 +189,13 @@ const selectedDate = computed(() => dayjs(rawSelectedDate.value))
|
||||
const endOfMonthDate = computed(() => selectedDate.value.endOf('month'))
|
||||
const withdrawalDate = computed(() => endOfMonthDate.value.add(60, 'days'))
|
||||
|
||||
const { data: transparencyInformation } = await useAsyncData('payout/platform_revenue', () =>
|
||||
useBaseFetch('payout/platform_revenue', {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
)
|
||||
const { data: transparencyInformation } = useQuery({
|
||||
queryKey: ['payout', 'platform_revenue'],
|
||||
queryFn: () =>
|
||||
useBaseFetch('payout/platform_revenue', {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
})
|
||||
|
||||
const platformRevenue = (transparencyInformation.value as any)?.all_time
|
||||
const platformRevenueData = (transparencyInformation.value as any)?.data?.slice(0, 5) ?? []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Report } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import ModerationReportCard from '~/components/ui/moderation/ModerationReportCard.vue'
|
||||
import { enrichReportBatch } from '~/helpers/moderation.ts'
|
||||
@@ -7,18 +8,21 @@ import { enrichReportBatch } from '~/helpers/moderation.ts'
|
||||
const { params } = useRoute()
|
||||
const reportId = params.id as string
|
||||
|
||||
const { data: report } = await useAsyncData(`moderation-report-${reportId}`, async () => {
|
||||
try {
|
||||
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report
|
||||
const enrichedReport = (await enrichReportBatch([report]))[0]
|
||||
return enrichedReport
|
||||
} catch (error) {
|
||||
console.error('Error fetching report:', error)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Report not found',
|
||||
})
|
||||
}
|
||||
const { data: report } = useQuery({
|
||||
queryKey: computed(() => ['report', reportId]),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const report = (await useBaseFetch(`report/${reportId}`, { apiVersion: 3 })) as Report
|
||||
const enrichedReport = (await enrichReportBatch([report]))[0]
|
||||
return enrichedReport
|
||||
} catch (error) {
|
||||
console.error('Error fetching report:', error)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Report not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { GitGraphIcon, RssIcon } from '@modrinth/assets'
|
||||
import { articles as rawArticles } from '@modrinth/blog'
|
||||
import { Avatar, ButtonStyled, useFormatDateTime } from '@modrinth/ui'
|
||||
import type { User } from '@modrinth/utils'
|
||||
import { Avatar, ButtonStyled, injectModrinthClient, useFormatDateTime } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import NewsletterButton from '~/components/ui/NewsletterButton.vue'
|
||||
import ShareArticleButtons from '~/components/ui/ShareArticleButtons.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -24,21 +25,19 @@ if (!rawArticle) {
|
||||
})
|
||||
}
|
||||
|
||||
const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}`
|
||||
const { data: authors } = useQuery({
|
||||
queryKey: computed(() => ['users', rawArticle.authors]),
|
||||
queryFn: async () => {
|
||||
const users = await client.labrinth.users_v2.getMultiple(rawArticle.authors)
|
||||
users.sort((a, b) => {
|
||||
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id)
|
||||
})
|
||||
return users
|
||||
},
|
||||
enabled: computed(() => rawArticle.authors.length > 0),
|
||||
})
|
||||
|
||||
const [authors, html] = await Promise.all([
|
||||
rawArticle.authors
|
||||
? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => {
|
||||
const users = data.data as Ref<User[]>
|
||||
users.value.sort((a, b) => {
|
||||
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id)
|
||||
})
|
||||
|
||||
return users
|
||||
})
|
||||
: Promise.resolve(),
|
||||
rawArticle.html(),
|
||||
])
|
||||
const html = await rawArticle.html()
|
||||
|
||||
const article = computed(() => ({
|
||||
...rawArticle,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div v-if="isLoading" class="flex min-h-[50vh] items-center justify-center">
|
||||
<SpinnerIcon class="h-12 w-12 animate-spin text-brand" />
|
||||
</div>
|
||||
<div
|
||||
v-if="organization"
|
||||
v-else-if="organization"
|
||||
class="experimental-styles-within new-page sidebar"
|
||||
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }"
|
||||
>
|
||||
@@ -240,10 +243,14 @@
|
||||
:downloads="project.downloads"
|
||||
:followers="project.followers"
|
||||
:tags="project.categories"
|
||||
:environment="{
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}"
|
||||
:environment="
|
||||
project.client_side && project.server_side
|
||||
? {
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
:status="
|
||||
auth.user && (auth.user.id! === user.id || tags.staffRoles.includes(auth.user.role))
|
||||
? (project.status as ProjectStatus)
|
||||
@@ -282,6 +289,7 @@ import {
|
||||
MoreVerticalIcon,
|
||||
OrganizationIcon,
|
||||
SettingsIcon,
|
||||
SpinnerIcon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
@@ -290,6 +298,7 @@ import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ContentPageHeader,
|
||||
injectModrinthClient,
|
||||
OverflowMenu,
|
||||
ProjectCard,
|
||||
ProjectCardList,
|
||||
@@ -298,6 +307,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
@@ -342,70 +352,95 @@ if (route.path.includes('settings')) {
|
||||
// hacky way to show the edit button on the corner of the card.
|
||||
const routeHasSettings = computed(() => route.path.includes('settings'))
|
||||
|
||||
const [
|
||||
{ data: organization, refresh: refreshOrganization },
|
||||
{ data: projects, refresh: refreshProjects },
|
||||
] = await Promise.all([
|
||||
useAsyncData(
|
||||
`organization/${orgId}`,
|
||||
() => useBaseFetch(`organization/${orgId}`, { apiVersion: 3 }) as Promise<Organization>,
|
||||
),
|
||||
useAsyncData(
|
||||
`organization/${orgId}/projects`,
|
||||
() => useBaseFetch(`organization/${orgId}/projects`, { apiVersion: 3 }) as Promise<ProjectV3[]>,
|
||||
{
|
||||
transform: (projects) => {
|
||||
for (const project of projects) {
|
||||
project.categories = project.categories.concat(project.loaders)
|
||||
const client = injectModrinthClient()
|
||||
|
||||
if (project.mrpack_loaders) {
|
||||
project.categories = project.categories.concat(project.mrpack_loaders)
|
||||
}
|
||||
const {
|
||||
data: organization,
|
||||
refetch: refreshOrganization,
|
||||
error: orgError,
|
||||
isPending: organizationIsPending,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['organization', orgId]),
|
||||
// @ts-expect-error
|
||||
queryFn: () => client.labrinth.organizations_v3.get(orgId),
|
||||
enabled: !!orgId,
|
||||
})
|
||||
|
||||
const singleplayer = project.singleplayer && project.singleplayer[0]
|
||||
const clientAndServer = project.client_and_server && project.client_and_server[0]
|
||||
const clientOnly = project.client_only && project.client_only[0]
|
||||
const serverOnly = project.server_only && project.server_only[0]
|
||||
watch(
|
||||
orgError,
|
||||
(error) => {
|
||||
if (error) {
|
||||
const status = (error as any).statusCode ?? (error as any).status ?? 404
|
||||
showError({
|
||||
fatal: true,
|
||||
statusCode: status,
|
||||
message: 'Organization not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// quick and dirty hack to show envs as legacy
|
||||
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
|
||||
project.client_side = 'required'
|
||||
project.server_side = 'required'
|
||||
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
|
||||
project.client_side = 'required'
|
||||
project.server_side = 'unsupported'
|
||||
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
|
||||
project.client_side = 'unsupported'
|
||||
project.server_side = 'required'
|
||||
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
|
||||
project.client_side = 'optional'
|
||||
project.server_side = 'optional'
|
||||
}
|
||||
}
|
||||
const {
|
||||
data: projects,
|
||||
refetch: refreshProjects,
|
||||
isFetching: projectsIsFetching,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['organization', orgId, 'projects']),
|
||||
queryFn: async () => {
|
||||
// @ts-expect-error
|
||||
const rawProjects = (await client.labrinth.organizations_v3.getProjects(orgId)) as ProjectV3[]
|
||||
|
||||
return projects
|
||||
},
|
||||
},
|
||||
),
|
||||
])
|
||||
return rawProjects.map((project) => {
|
||||
let categories = project.categories.concat(project.loaders)
|
||||
if (project.mrpack_loaders) {
|
||||
categories = categories.concat(project.mrpack_loaders as string[])
|
||||
}
|
||||
|
||||
const singleplayer = project.singleplayer && (project.singleplayer as string[])[0]
|
||||
const clientAndServer =
|
||||
project.client_and_server && (project.client_and_server as string[])[0]
|
||||
const clientOnly = project.client_only && (project.client_only as string[])[0]
|
||||
const serverOnly = project.server_only && (project.server_only as string[])[0]
|
||||
|
||||
let client_side: ProjectV3['client_side'] | undefined
|
||||
let server_side: ProjectV3['server_side'] | undefined
|
||||
|
||||
// quick and dirty hack to show envs as legacy
|
||||
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
|
||||
client_side = 'required'
|
||||
server_side = 'required'
|
||||
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
|
||||
client_side = 'required'
|
||||
server_side = 'unsupported'
|
||||
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
|
||||
client_side = 'unsupported'
|
||||
server_side = 'required'
|
||||
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
|
||||
client_side = 'optional'
|
||||
server_side = 'optional'
|
||||
}
|
||||
|
||||
return { ...project, categories, client_side, server_side }
|
||||
})
|
||||
},
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
await Promise.all([refreshOrganization(), refreshProjects()])
|
||||
}
|
||||
|
||||
if (!organization.value) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: 'Organization not found',
|
||||
})
|
||||
}
|
||||
// Loading state
|
||||
const isLoading = computed(() => {
|
||||
return organizationIsPending.value || projectsIsFetching.value
|
||||
})
|
||||
|
||||
// Filter accepted, sort by role, then by name and Owner role always goes first
|
||||
const acceptedMembers = computed(() => {
|
||||
const acceptedMembers = organization.value?.members?.filter((x) => x.accepted) ?? []
|
||||
const owner = acceptedMembers.find((x) => x.is_owner)
|
||||
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
|
||||
const rest = acceptedMembers.filter((x) => !x.is_owner) ?? []
|
||||
|
||||
rest.sort((a, b) => {
|
||||
if (a.role === b.role) {
|
||||
@@ -415,7 +450,7 @@ const acceptedMembers = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
return [owner, ...rest]
|
||||
return owner ? [owner, ...rest] : rest
|
||||
})
|
||||
|
||||
const isInvited = computed(() => {
|
||||
@@ -477,21 +512,35 @@ const onDeclineInvite = useClientTry(async () => {
|
||||
await refreshOrganization()
|
||||
})
|
||||
|
||||
const organizationContext = new OrganizationContext(organization, projects, auth, tags, refresh)
|
||||
const organizationContext = new OrganizationContext(
|
||||
organization as Ref<Organization | null>,
|
||||
projects as Ref<ProjectV3[] | null>,
|
||||
auth,
|
||||
tags,
|
||||
refresh,
|
||||
)
|
||||
const { currentMember } = organizationContext
|
||||
|
||||
provideOrganizationContext(organizationContext)
|
||||
|
||||
const title = `${organization.value.name} - Organization`
|
||||
const description = `${organization.value.description} - View the organization ${organization.value.name} on Modrinth`
|
||||
watch(
|
||||
organization,
|
||||
(org) => {
|
||||
if (org) {
|
||||
const title = `${org.name} - Organization`
|
||||
const description = `${org.description} - View the organization ${org.name} on Modrinth`
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: organization.value.description,
|
||||
ogImage: organization.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
})
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: org.description,
|
||||
ogImage: org.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const navLinks = computed(() => [
|
||||
{
|
||||
|
||||
@@ -334,6 +334,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
|
||||
@@ -347,13 +348,12 @@ const { organization, projects, refresh } = injectOrganizationContext()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const { data: userProjects, refresh: refreshUserProjects } = await useAsyncData(
|
||||
`user/${auth.value.user.id}/projects`,
|
||||
() => useBaseFetch(`user/${auth.value.user.id}/projects`),
|
||||
{
|
||||
watch: [auth],
|
||||
},
|
||||
)
|
||||
const { data: userProjects, refetch: refreshUserProjects } = useQuery({
|
||||
queryKey: computed(() => ['user', auth.value?.user?.id, 'projects']),
|
||||
queryFn: () => useBaseFetch(`user/${auth.value.user.id}/projects`),
|
||||
enabled: computed(() => !!auth.value?.user?.id),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
const usersOwnedProjects = ref([])
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ import {
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import {
|
||||
@@ -491,16 +492,14 @@ const loading = ref(false)
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const { data: usersApps, refresh } = await useAsyncData(
|
||||
'usersApps',
|
||||
() =>
|
||||
const { data: usersApps, refetch: refresh } = useQuery({
|
||||
queryKey: computed(() => ['user', auth.value?.user?.id, 'oauth_apps']),
|
||||
queryFn: () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/oauth_apps`, {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
{
|
||||
watch: [auth],
|
||||
},
|
||||
)
|
||||
enabled: computed(() => !!auth.value?.user?.id),
|
||||
})
|
||||
|
||||
const setForm = (app) => {
|
||||
if (app?.id) {
|
||||
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import { useScopes } from '~/composables/auth/scopes.ts'
|
||||
|
||||
@@ -116,42 +117,36 @@ useHead({
|
||||
title: 'Authorizations - Modrinth',
|
||||
})
|
||||
|
||||
const { data: usersApps, refresh } = await useAsyncData('userAuthorizations', () =>
|
||||
useBaseFetch(`oauth/authorizations`, {
|
||||
internal: true,
|
||||
}),
|
||||
)
|
||||
const { data: usersApps, refetch: refresh } = useQuery({
|
||||
queryKey: ['oauth', 'authorizations'],
|
||||
queryFn: () =>
|
||||
useBaseFetch(`oauth/authorizations`, {
|
||||
internal: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const { data: appInformation } = await useAsyncData(
|
||||
'appInfo',
|
||||
() => {
|
||||
if (!usersApps.value?.length) return null
|
||||
return useBaseFetch('oauth/apps', {
|
||||
const { data: appInformation } = useQuery({
|
||||
queryKey: computed(() => ['oauth', 'apps', usersApps.value?.map((c) => c.app_id)]),
|
||||
queryFn: () =>
|
||||
useBaseFetch('oauth/apps', {
|
||||
internal: true,
|
||||
query: {
|
||||
ids: JSON.stringify(usersApps.value.map((c) => c.app_id)),
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
watch: usersApps,
|
||||
},
|
||||
)
|
||||
}),
|
||||
enabled: computed(() => !!usersApps.value?.length),
|
||||
})
|
||||
|
||||
const { data: appCreatorsInformation } = await useAsyncData(
|
||||
'appCreatorsInfo',
|
||||
() => {
|
||||
if (!appInformation.value?.length) return null
|
||||
return useBaseFetch('users', {
|
||||
const { data: appCreatorsInformation } = useQuery({
|
||||
queryKey: computed(() => ['users', appInformation.value?.map((c) => c.created_by)]),
|
||||
queryFn: () =>
|
||||
useBaseFetch('users', {
|
||||
query: {
|
||||
ids: JSON.stringify(appInformation.value.map((c) => c.created_by)),
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
watch: appInformation,
|
||||
},
|
||||
)
|
||||
}),
|
||||
enabled: computed(() => !!appInformation.value?.length),
|
||||
})
|
||||
|
||||
const appInfoLookup = computed(() => {
|
||||
if (!usersApps.value || !appInformation.value || !appCreatorsInformation.value) {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Badge, Breadcrumbs, useFormatDateTime, useFormatPrice } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import { products } from '~/generated/state.json'
|
||||
|
||||
@@ -53,23 +54,22 @@ const formatDate = useFormatDateTime({
|
||||
day: '2-digit',
|
||||
})
|
||||
|
||||
const { data: charges } = await useAsyncData(
|
||||
'billing/payments',
|
||||
() => useBaseFetch('billing/payments', { internal: true }),
|
||||
{
|
||||
transform: (charges) => {
|
||||
return charges
|
||||
.filter((charge) => charge.status !== 'open' && charge.status !== 'cancelled')
|
||||
.map((charge) => {
|
||||
const product = products.find((product) =>
|
||||
product.prices.some((price) => price.id === charge.price_id),
|
||||
)
|
||||
const { data: charges } = useQuery({
|
||||
queryKey: ['billing', 'payments'],
|
||||
queryFn: async () => {
|
||||
const charges = await useBaseFetch('billing/payments', { internal: true })
|
||||
return charges
|
||||
.filter((charge) => charge.status !== 'open' && charge.status !== 'cancelled')
|
||||
.map((charge) => {
|
||||
const product = products.find((product) =>
|
||||
product.prices.some((price) => price.id === charge.price_id),
|
||||
)
|
||||
|
||||
charge.product = product
|
||||
charge.product = product
|
||||
|
||||
return charge
|
||||
})
|
||||
},
|
||||
return charge
|
||||
})
|
||||
},
|
||||
)
|
||||
placeholderData: [],
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -299,12 +299,14 @@
|
||||
<div class="flex text-2xl font-bold text-contrast">
|
||||
<span class="text-contrast">
|
||||
{{
|
||||
formatPrice(
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
.prices.intervals[subscription.interval],
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
.currency_code,
|
||||
)
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
? formatPrice(
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
.prices.intervals[subscription.interval],
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
.currency_code,
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span>/{{ subscription.interval.replace('ly', '') }}</span>
|
||||
@@ -450,6 +452,7 @@
|
||||
@proceed="removePaymentMethod(removePaymentMethodIndex)"
|
||||
/>
|
||||
<PurchaseModal
|
||||
v-if="customer && paymentMethods"
|
||||
ref="midasPurchaseModal"
|
||||
:product="midasProduct"
|
||||
:country="country"
|
||||
@@ -620,6 +623,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { calculateSavings, getCurrency } from '@modrinth/utils'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '@/composables/fetch.js'
|
||||
@@ -734,25 +738,32 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const [
|
||||
{ data: paymentMethods, refresh: refreshPaymentMethods },
|
||||
{ data: charges, refresh: refreshCharges },
|
||||
{ data: customer, refresh: refreshCustomer },
|
||||
{ data: subscriptions, refresh: refreshSubscriptions },
|
||||
{ data: productsData, refresh: refreshProducts },
|
||||
{ data: serversData, refresh: refreshServers },
|
||||
] = await Promise.all([
|
||||
useAsyncData('billing/payment_methods', () =>
|
||||
useBaseFetch('billing/payment_methods', { internal: true }),
|
||||
),
|
||||
useAsyncData('billing/payments', () => useBaseFetch('billing/payments', { internal: true })),
|
||||
useAsyncData('billing/customer', () => useBaseFetch('billing/customer', { internal: true })),
|
||||
useAsyncData('billing/subscriptions', () =>
|
||||
useBaseFetch('billing/subscriptions', { internal: true }),
|
||||
),
|
||||
useAsyncData('billing/products', () => useBaseFetch('billing/products', { internal: true })),
|
||||
useAsyncData('servers', () => useServersFetch('servers')),
|
||||
])
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: paymentMethods } = useQuery({
|
||||
queryKey: ['billing', 'payment_methods'],
|
||||
queryFn: () => useBaseFetch('billing/payment_methods', { internal: true }),
|
||||
})
|
||||
const { data: charges } = useQuery({
|
||||
queryKey: ['billing', 'payments'],
|
||||
queryFn: () => useBaseFetch('billing/payments', { internal: true }),
|
||||
})
|
||||
const { data: customer } = useQuery({
|
||||
queryKey: ['billing', 'customer'],
|
||||
queryFn: () => useBaseFetch('billing/customer', { internal: true }),
|
||||
})
|
||||
const { data: subscriptions } = useQuery({
|
||||
queryKey: ['billing', 'subscriptions'],
|
||||
queryFn: () => useBaseFetch('billing/subscriptions', { internal: true }),
|
||||
})
|
||||
const { data: productsData } = useQuery({
|
||||
queryKey: ['billing', 'products'],
|
||||
queryFn: () => useBaseFetch('billing/products', { internal: true }),
|
||||
})
|
||||
const { data: serversData } = useQuery({
|
||||
queryKey: ['servers'],
|
||||
queryFn: () => useServersFetch('servers'),
|
||||
})
|
||||
|
||||
const midasProduct = ref(products.find((x) => x.metadata?.type === 'midas'))
|
||||
const midasSubscription = computed(() =>
|
||||
@@ -996,12 +1007,8 @@ const resubscribePyro = async (subscriptionId, wasSuspended) => {
|
||||
|
||||
const refresh = async () => {
|
||||
await Promise.all([
|
||||
refreshPaymentMethods(),
|
||||
refreshCharges(),
|
||||
refreshCustomer(),
|
||||
refreshSubscriptions(),
|
||||
refreshProducts(),
|
||||
refreshServers(),
|
||||
queryClient.invalidateQueries({ queryKey: ['billing'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['servers'] }),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ import {
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import {
|
||||
@@ -326,7 +327,11 @@ const deletePatIndex = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const { data: pats, refresh } = await useAsyncData('pat', () => useBaseFetch('pat'))
|
||||
const { data: pats, refetch: refresh } = useQuery({
|
||||
queryKey: ['pat'],
|
||||
queryFn: () => useBaseFetch('pat'),
|
||||
placeholderData: [],
|
||||
})
|
||||
const displayPats = computed(() => {
|
||||
return pats.value.toSorted((a, b) => new Date(b.created) - new Date(a.created))
|
||||
})
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
@@ -101,9 +102,10 @@ useHead({
|
||||
title: () => `${formatMessage(commonSettingsMessages.sessions)} - Modrinth`,
|
||||
})
|
||||
|
||||
const { data: sessions, refresh } = await useAsyncData('session/list', () =>
|
||||
useBaseFetch('session/list'),
|
||||
)
|
||||
const { data: sessions, refetch: refresh } = useQuery({
|
||||
queryKey: ['session', 'list'],
|
||||
queryFn: () => useBaseFetch('session/list'),
|
||||
})
|
||||
|
||||
async function revokeSession(id) {
|
||||
startLoading()
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||
<NavTabs :links="navLinks" replace />
|
||||
</div>
|
||||
<div v-if="projects.length > 0">
|
||||
<div v-if="projects?.length > 0">
|
||||
<ProjectCardList
|
||||
v-if="route.params.projectType !== 'collections'"
|
||||
:layout="cosmetics.searchDisplayMode.user"
|
||||
@@ -331,7 +331,7 @@
|
||||
<div
|
||||
v-else-if="
|
||||
(route.params.projectType && route.params.projectType !== 'collections') ||
|
||||
(!route.params.projectType && collections.length === 0)
|
||||
(!route.params.projectType && collections?.length === 0)
|
||||
"
|
||||
class="error"
|
||||
>
|
||||
@@ -353,7 +353,7 @@
|
||||
class="collections-grid"
|
||||
>
|
||||
<nuxt-link
|
||||
v-for="collection in collections.sort(
|
||||
v-for="collection in (collections ?? []).sort(
|
||||
(a, b) => new Date(b.created) - new Date(a.created),
|
||||
)"
|
||||
:key="collection.id"
|
||||
@@ -404,7 +404,7 @@
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="route.params.projectType === 'collections' && collections.length === 0"
|
||||
v-if="route.params.projectType === 'collections' && collections?.length === 0"
|
||||
class="error"
|
||||
>
|
||||
<UpToDate class="icon" />
|
||||
@@ -425,7 +425,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<div v-if="organizations.length > 0" class="card flex-card">
|
||||
<div v-if="organizations?.length > 0" class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">
|
||||
{{ formatMessage(messages.profileOrganizations) }}
|
||||
</h2>
|
||||
@@ -492,6 +492,7 @@ import {
|
||||
commonMessages,
|
||||
ContentPageHeader,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
@@ -506,6 +507,8 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
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'
|
||||
@@ -527,6 +530,7 @@ const auth = await useAuth()
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useGeneratedState()
|
||||
const config = useRuntimeConfig()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatNumber = useFormatNumber()
|
||||
@@ -679,74 +683,81 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
let user, projects, organizations, collections, refreshUser
|
||||
try {
|
||||
;[
|
||||
{ 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,
|
||||
)
|
||||
}
|
||||
const client = injectModrinthClient()
|
||||
|
||||
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 }),
|
||||
),
|
||||
const {
|
||||
data: user,
|
||||
error: userError,
|
||||
suspense: userSuspense,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['user', route.params.id]),
|
||||
queryFn: () => client.labrinth.users_v2.get(route.params.id),
|
||||
})
|
||||
|
||||
watch(
|
||||
userError,
|
||||
(error) => {
|
||||
if (error) {
|
||||
const status = error.statusCode ?? error.status ?? 404
|
||||
showError({
|
||||
fatal: true,
|
||||
statusCode: status,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const { data: projects, suspense: projectsSuspense } = useQuery({
|
||||
queryKey: computed(() => ['user', route.params.id, 'projects']),
|
||||
queryFn: async () => {
|
||||
const projects = await client.labrinth.users_v2.getProjects(route.params.id)
|
||||
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
|
||||
},
|
||||
})
|
||||
|
||||
const { data: organizations, suspense: orgsSuspense } = useQuery({
|
||||
queryKey: computed(() => ['user', route.params.id, 'organizations']),
|
||||
queryFn: () => client.labrinth.users_v2.getOrganizations(route.params.id),
|
||||
})
|
||||
|
||||
const { data: collections, suspense: collectionsSuspense } = useQuery({
|
||||
queryKey: computed(() => ['user', route.params.id, 'collections']),
|
||||
queryFn: () => client.labrinth.users_v2.getCollections(route.params.id),
|
||||
})
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
await Promise.allSettled([
|
||||
userSuspense(),
|
||||
projectsSuspense(),
|
||||
orgsSuspense(),
|
||||
collectionsSuspense(),
|
||||
])
|
||||
} catch {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const sortedOrgs = computed(() =>
|
||||
organizations.value ? [...organizations.value].sort((a, b) => a.name.localeCompare(b.name)) : [],
|
||||
)
|
||||
|
||||
if (!user.value) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
})
|
||||
}
|
||||
|
||||
if (user.value.username !== route.params.id) {
|
||||
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
|
||||
}
|
||||
|
||||
const title = computed(() => `${user.value.username} - Modrinth`)
|
||||
const title = computed(() => (user.value ? `${user.value.username} - Modrinth` : 'Modrinth'))
|
||||
const description = computed(() =>
|
||||
user.value.bio
|
||||
user.value?.bio
|
||||
? formatMessage(messages.profileMetaDescriptionWithBio, {
|
||||
bio: user.value.bio,
|
||||
username: user.value.username,
|
||||
})
|
||||
: formatMessage(messages.profileMetaDescription, { username: user.value.username }),
|
||||
: user.value
|
||||
? formatMessage(messages.profileMetaDescription, { username: user.value.username })
|
||||
: '',
|
||||
)
|
||||
|
||||
useSeoMeta({
|
||||
@@ -754,7 +765,7 @@ useSeoMeta({
|
||||
description: () => description.value,
|
||||
ogTitle: () => title.value,
|
||||
ogDescription: () => description.value,
|
||||
ogImage: () => user.value.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
ogImage: () => user.value?.avatar_url ?? 'https://cdn.modrinth.com/placeholder.png',
|
||||
})
|
||||
|
||||
const projectTypes = computed(() => {
|
||||
@@ -838,15 +849,12 @@ async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
|
||||
}
|
||||
|
||||
const isAffiliate = computed(() => user.value.badges & UserBadge.AFFILIATE)
|
||||
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()
|
||||
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
|
||||
queryClient.invalidateQueries({ queryKey: ['user', route.params.id] })
|
||||
}
|
||||
|
||||
const navLinks = computed(() => [
|
||||
@@ -865,7 +873,7 @@ const navLinks = computed(() => [
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
])
|
||||
|
||||
const selectedRole = ref(user.value.role)
|
||||
const selectedRole = ref(user.value?.role)
|
||||
const isSavingRole = ref(false)
|
||||
|
||||
const roleOptions = [
|
||||
@@ -893,12 +901,8 @@ function saveRoleEdit() {
|
||||
|
||||
isSavingRole.value = true
|
||||
|
||||
useBaseFetch(`user/${user.value.id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
role: selectedRole.value,
|
||||
},
|
||||
})
|
||||
client.labrinth.users_v2
|
||||
.patch(user.value.id, { role: selectedRole.value })
|
||||
.then(() => {
|
||||
user.value.role = selectedRole.value
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('app:error', (error: any) => {
|
||||
console.error('An error occurred:', error)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ export default defineNuxtPlugin((nuxt) => {
|
||||
const vueQueryState = useState<DehydratedState | null>('vue-query')
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 5000 } },
|
||||
defaultOptions: { queries: { staleTime: 10000 } },
|
||||
})
|
||||
const options: VueQueryPluginOptions = { queryClient }
|
||||
|
||||
|
||||
5
apps/frontend/src/server/plugins/error-handling.ts
Normal file
5
apps/frontend/src/server/plugins/error-handling.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('error', async (error, { event }) => {
|
||||
console.error(`[Context Error] at ${event?.path}:`, error)
|
||||
})
|
||||
})
|
||||
@@ -10,8 +10,11 @@ import { ISO3166Module } from './iso3166'
|
||||
import { KyrosContentV1Module } from './kyros/content/v1'
|
||||
import { KyrosFilesV0Module } from './kyros/files/v0'
|
||||
import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth'
|
||||
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
|
||||
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
||||
import { LabrinthCollectionsModule } from './labrinth/collections'
|
||||
import { LabrinthOrganizationsV3Module } from './labrinth/organizations/v3'
|
||||
import { LabrinthPayoutV3Module } from './labrinth/payout/v3'
|
||||
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
|
||||
import { LabrinthProjectsV3Module } from './labrinth/projects/v3'
|
||||
import { LabrinthServerPingInternalModule } from './labrinth/server-ping/internal'
|
||||
@@ -45,8 +48,11 @@ export const MODULE_REGISTRY = {
|
||||
launchermeta_manifest_v0: LauncherMetaManifestV0Module,
|
||||
kyros_content_v1: KyrosContentV1Module,
|
||||
kyros_files_v0: KyrosFilesV0Module,
|
||||
labrinth_auth_internal: LabrinthAuthInternalModule,
|
||||
labrinth_billing_internal: LabrinthBillingInternalModule,
|
||||
labrinth_collections: LabrinthCollectionsModule,
|
||||
labrinth_organizations_v3: LabrinthOrganizationsV3Module,
|
||||
labrinth_payout_v3: LabrinthPayoutV3Module,
|
||||
labrinth_projects_v2: LabrinthProjectsV2Module,
|
||||
labrinth_projects_v3: LabrinthProjectsV3Module,
|
||||
labrinth_server_ping_internal: LabrinthServerPingInternalModule,
|
||||
|
||||
32
packages/api-client/src/modules/labrinth/auth/internal.ts
Normal file
32
packages/api-client/src/modules/labrinth/auth/internal.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthAuthInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_auth_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is subscribed to the newsletter
|
||||
*
|
||||
* @returns Promise resolving to the subscription status
|
||||
*/
|
||||
public async getNewsletterStatus(): Promise<Labrinth.Auth.Internal.SubscriptionStatus> {
|
||||
return this.client.request<Labrinth.Auth.Internal.SubscriptionStatus>('/auth/email/subscribe', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the newsletter
|
||||
*/
|
||||
public async subscribeNewsletter(): Promise<void> {
|
||||
return this.client.request('/auth/email/subscribe', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export * from './auth/internal'
|
||||
export * from './billing/internal'
|
||||
export * from './collections'
|
||||
export * from './organizations/v3'
|
||||
export * from './payout/v3'
|
||||
export * from './projects/v2'
|
||||
export * from './projects/v3'
|
||||
export * from './server-ping/internal'
|
||||
|
||||
52
packages/api-client/src/modules/labrinth/organizations/v3.ts
Normal file
52
packages/api-client/src/modules/labrinth/organizations/v3.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthOrganizationsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_organizations_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an organization by ID or slug
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @returns Promise resolving to the organization data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const org = await client.labrinth.organizations_v3.get('my-org')
|
||||
* ```
|
||||
*/
|
||||
public async get(idOrSlug: string): Promise<Labrinth.Organizations.v3.Organization> {
|
||||
return this.client.request<Labrinth.Organizations.v3.Organization>(
|
||||
`/organization/${idOrSlug}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an organization's projects
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @returns Promise resolving to the organization's projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.organizations_v3.getProjects('my-org')
|
||||
* ```
|
||||
*/
|
||||
public async getProjects(idOrSlug: string): Promise<Labrinth.Projects.v3.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v3.Project[]>(
|
||||
`/organization/${idOrSlug}/projects`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
21
packages/api-client/src/modules/labrinth/payout/v3.ts
Normal file
21
packages/api-client/src/modules/labrinth/payout/v3.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthPayoutV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_payout_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's payout balance
|
||||
*
|
||||
* @returns Promise resolving to the user's payout balance
|
||||
*/
|
||||
public async getBalance(): Promise<Labrinth.Payout.v3.PayoutBalance> {
|
||||
return this.client.request<Labrinth.Payout.v3.PayoutBalance>('/payout/balance', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -240,4 +240,19 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
||||
params: { url },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random projects
|
||||
*
|
||||
* @param count - Number of random projects to return
|
||||
* @returns Promise resolving to an array of random projects
|
||||
*/
|
||||
public async getRandom(count: number): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v2.Project[]>('/projects_random', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: { count: String(count) },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,28 @@ export namespace Labrinth {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Payout {
|
||||
export namespace v3 {
|
||||
export type PayoutBalance = {
|
||||
available: number
|
||||
withdrawn_lifetime: number
|
||||
withdrawn_ytd: number
|
||||
pending: number
|
||||
dates: Record<string, number>
|
||||
requested_form_type: string | null
|
||||
form_completion_status: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Auth {
|
||||
export namespace Internal {
|
||||
export type SubscriptionStatus = {
|
||||
subscribed: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Projects {
|
||||
export namespace v2 {
|
||||
export type Environment = 'required' | 'optional' | 'unsupported' | 'unknown'
|
||||
|
||||
@@ -6,6 +6,47 @@ export class LabrinthUsersV2Module extends AbstractModule {
|
||||
return 'labrinth_users_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID or username
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to the user data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const user = await client.labrinth.users_v2.get('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async get(idOrUsername: string): Promise<Labrinth.Users.v2.User> {
|
||||
return this.client.request<Labrinth.Users.v2.User>(`/user/${idOrUsername}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple users by their IDs
|
||||
*
|
||||
* @param ids - Array of user IDs
|
||||
* @returns Promise resolving to an array of users
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const users = await client.labrinth.users_v2.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Users.v2.User[]> {
|
||||
return this.client.request<Labrinth.Users.v2.User[]>(
|
||||
`/users?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's projects
|
||||
*
|
||||
@@ -24,4 +65,73 @@ export class LabrinthUsersV2Module extends AbstractModule {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's organizations
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's organizations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const orgs = await client.labrinth.users_v2.getOrganizations('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getOrganizations(
|
||||
idOrUsername: string,
|
||||
): Promise<Labrinth.Organizations.v3.Organization[]> {
|
||||
return this.client.request<Labrinth.Organizations.v3.Organization[]>(
|
||||
`/user/${idOrUsername}/organizations`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's collections
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's collections
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const collections = await client.labrinth.users_v2.getCollections('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getCollections(idOrUsername: string): Promise<Labrinth.Collections.Collection[]> {
|
||||
return this.client.request<Labrinth.Collections.Collection[]>(
|
||||
`/user/${idOrUsername}/collections`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @param data - Fields to update
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.users_v2.patch('my_user', { role: 'admin' })
|
||||
* ```
|
||||
*/
|
||||
public async patch(
|
||||
idOrUsername: string,
|
||||
data: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'role'>>,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/user/${idOrUsername}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user