Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fd9aa86
refactor: most places with useAsyncData replaced with tanstack query
tdgao Feb 1, 2026
98d95b6
refactor report list and report view
tdgao Feb 2, 2026
4cd2150
refactor organization page to use tanstack query
tdgao Feb 2, 2026
3324643
fix types
tdgao Feb 2, 2026
ce639de
refactor collection page and include proper loading state
tdgao Feb 2, 2026
40d9865
fix followed projects proper loading state
tdgao Feb 2, 2026
4930830
fix 404 handling
tdgao Feb 2, 2026
56167a3
fix organization loading and 404 states
tdgao Feb 2, 2026
d65db6a
pnpm prepr
tdgao Feb 2, 2026
36da8cc
Merge branch 'main' into truman/ditching-useAsyncData
tdgao Feb 14, 2026
eb2ddd6
refactor: remove useAsyncData on newsletter button
tdgao Feb 14, 2026
80d4676
refactor: remove useAsyncData on auth globals fetch
tdgao Feb 14, 2026
3dda4f2
refactor: settings/billing/index.vue to useQuery instead of useAsyncData
tdgao Feb 14, 2026
240d6eb
refactor: user page to remove useAsyncData
tdgao Feb 14, 2026
96a56eb
Merge branch 'main' into truman/ditching-useAsyncData
tdgao Mar 13, 2026
4fd2de0
pnpm prepr
tdgao Mar 13, 2026
575a2df
fix reports pages
tdgao Mar 13, 2026
0daf7a4
fix notifications page
tdgao Mar 13, 2026
fb61801
fix billing page cannot read properties of null and prop warnings
tdgao Mar 13, 2026
6b8978d
fix refresh causing 404 by removing useBaseFetch and use api-client
tdgao Mar 13, 2026
268c753
fix stale data after removing organization from project
tdgao Mar 13, 2026
abdf0f2
Merge branch 'main' into truman/ditching-useAsyncData
tdgao Mar 13, 2026
0135ae9
pnpm prepr
tdgao Mar 13, 2026
6faee62
fix news erroring in build
tdgao Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 39 additions & 19 deletions apps/frontend/src/components/ui/NewsletterButton.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
<script setup lang="ts">
import { CheckIcon, MailIcon } from '@modrinth/assets'
import { ButtonStyled } from '@modrinth/ui'
import { ref } from 'vue'
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'

import { useBaseFetch } from '~/composables/fetch.js'

const auth = await useAuth()
const { formatMessage } = useVIntl()

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 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', {
const { subscribed } = (await useBaseFetch('auth/email/subscribe', {
method: 'GET',
})
})) as { subscribed: boolean }
return !subscribed
} catch {
return true
Expand All @@ -22,8 +45,8 @@ const showSubscribeButton = useAsyncData(
return false
}
},
{ watch: [auth], server: false },
)
enabled: computed(() => !!auth.value?.user),
})

async function subscribe() {
try {
Expand All @@ -36,22 +59,19 @@ async function subscribe() {
} 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>
11 changes: 7 additions & 4 deletions apps/frontend/src/components/ui/create/CreateLimitAlert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down
49 changes: 26 additions & 23 deletions apps/frontend/src/components/ui/report/ReportInfo.vue
Original file line number Diff line number Diff line change
@@ -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
}`"
Expand Down Expand Up @@ -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 }">
Expand All @@ -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>
Expand Down
132 changes: 76 additions & 56 deletions apps/frontend/src/components/ui/report/ReportView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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({
Expand All @@ -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}`),
)
const thread = computed(() => addReportMessage(rawThread.value, report.value))
// 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
})

async function updateThread(newThread) {
rawThread.value = newThread
report.value = await fetchReport()
}
// 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),
})

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, '')
// Version ID if applicable
const versionId = computed(() =>
rawReport.value?.item_type === 'version' ? rawReport.value.item_id : null,
)

const userIds = []
userIds.push(rawReport.value.reporter)
if (rawReport.value.item_type === 'user') {
userIds.push(rawReport.value.item_id)
}
// Fetch version
const { data: version } = useQuery({
queryKey: computed(() => ['version', versionId.value]),
queryFn: () => useBaseFetch(`version/${versionId.value}`),
enabled: computed(() => !!versionId.value),
})

const versionId = rawReport.value.item_type === 'version' ? rawReport.value.item_id : null
// 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
})

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
}
// Fetch project
const { data: project } = useQuery({
queryKey: computed(() => ['project', projectId.value]),
queryFn: () => useBaseFetch(`project/${projectId.value}`),
enabled: computed(() => !!projectId.value),
})

let version = null
if (versionId) {
const { data: versionVal } = await useAsyncData(`version/${versionId}`, () =>
useBaseFetch(`version/${versionId}`),
)
version = versionVal.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,
}
})

const projectId = version
? version.project_id
: rawReport.value.item_type === 'project'
? rawReport.value.item_id
: null
// 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),
})

let project = null
if (projectId) {
const { data: projectVal } = await useAsyncData(`project/${projectId}`, () =>
useBaseFetch(`project/${projectId}`),
)
project = projectVal.value
}
const thread = computed(() =>
rawThread.value && report.value ? addReportMessage(rawThread.value, report.value) : null,
)

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
async function updateThread(newThread) {
queryClient.setQueryData(['thread', report.value?.thread_id], newThread)
await queryClient.invalidateQueries({ queryKey: ['report', props.reportId] })
}
</script>
<style lang="scss" scoped>
Expand Down
Loading
Loading