mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
add loading states for creators + server details (#7033)
* add loading states for creators + server details * prepr
This commit is contained in:
@@ -432,7 +432,8 @@
|
|||||||
|
|
||||||
<div class="normal-page__sidebar">
|
<div class="normal-page__sidebar">
|
||||||
<ProjectSidebarServerInfo
|
<ProjectSidebarServerInfo
|
||||||
v-if="isServerProject && serverDataLoaded"
|
v-if="isServerProject"
|
||||||
|
:loading="!serverDataLoaded"
|
||||||
:project-v3="projectV3"
|
:project-v3="projectV3"
|
||||||
:tags="tags"
|
:tags="tags"
|
||||||
:required-content="serverRequiredContent"
|
:required-content="serverRequiredContent"
|
||||||
@@ -462,6 +463,7 @@
|
|||||||
<ProjectSidebarCreators
|
<ProjectSidebarCreators
|
||||||
:organization="organization"
|
:organization="organization"
|
||||||
:members="members"
|
:members="members"
|
||||||
|
:loading="creatorsLoading"
|
||||||
:org-link="(slug) => `/organization/${slug}`"
|
:org-link="(slug) => `/organization/${slug}`"
|
||||||
:user-link="(username) => `/user/${username}`"
|
:user-link="(username) => `/user/${username}`"
|
||||||
class="card flex-card"
|
class="card flex-card"
|
||||||
@@ -1023,7 +1025,11 @@ watch(serverModpackVersionId, (versionId) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Members
|
// Members
|
||||||
const { data: allMembersRaw, error: _membersError } = useQuery({
|
const {
|
||||||
|
data: allMembersRaw,
|
||||||
|
error: _membersError,
|
||||||
|
isPending: membersPending,
|
||||||
|
} = useQuery({
|
||||||
queryKey: computed(() => ['project', projectId.value, 'members']),
|
queryKey: computed(() => ['project', projectId.value, 'members']),
|
||||||
queryFn: () => client.labrinth.projects_v3.getMembers(projectId.value),
|
queryFn: () => client.labrinth.projects_v3.getMembers(projectId.value),
|
||||||
staleTime: STALE_TIME,
|
staleTime: STALE_TIME,
|
||||||
@@ -1074,7 +1080,7 @@ const {
|
|||||||
|
|
||||||
// Organization
|
// Organization
|
||||||
// Only fetch organization if project belongs to one
|
// Only fetch organization if project belongs to one
|
||||||
const { data: organizationRaw } = useQuery({
|
const { data: organizationRaw, isPending: organizationPending } = useQuery({
|
||||||
queryKey: computed(() => ['project', projectId.value, 'organization']),
|
queryKey: computed(() => ['project', projectId.value, 'organization']),
|
||||||
queryFn: () => client.labrinth.projects_v3.getOrganization(projectId.value),
|
queryFn: () => client.labrinth.projects_v3.getOrganization(projectId.value),
|
||||||
staleTime: STALE_TIME,
|
staleTime: STALE_TIME,
|
||||||
@@ -1085,6 +1091,13 @@ const { data: organizationRaw } = useQuery({
|
|||||||
// Return null when the project no longer belongs to an organization.
|
// Return null when the project no longer belongs to an organization.
|
||||||
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
|
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
|
||||||
|
|
||||||
|
const creatorsLoading = computed(
|
||||||
|
() =>
|
||||||
|
!projectRaw.value ||
|
||||||
|
membersPending.value ||
|
||||||
|
(!!projectRaw.value.organization && organizationPending.value),
|
||||||
|
)
|
||||||
|
|
||||||
const { data: thread } = useQuery({
|
const { data: thread } = useQuery({
|
||||||
queryKey: computed(() => ['thread', projectRaw.value?.thread_id]),
|
queryKey: computed(() => ['thread', projectRaw.value?.thread_id]),
|
||||||
queryFn: () => client.labrinth.threads_v3.getThread(projectRaw.value.thread_id),
|
queryFn: () => client.labrinth.threads_v3.getThread(projectRaw.value.thread_id),
|
||||||
|
|||||||
@@ -2,45 +2,59 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<h2 class="text-lg font-semibold m-0">{{ formatMessage(messages.title) }}</h2>
|
<h2 class="text-lg font-semibold m-0">{{ formatMessage(messages.title) }}</h2>
|
||||||
<div class="flex flex-col gap-3 font-semibold">
|
<div class="flex flex-col gap-3 font-semibold">
|
||||||
<template v-if="organization">
|
<template v-if="loading">
|
||||||
|
<div v-for="i in 2" :key="`creator-skeleton-${i}`" class="flex gap-2 items-center">
|
||||||
|
<div class="size-[32px] rounded-full bg-surface-4 animate-pulse"></div>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<div class="w-24 h-4 rounded-full bg-surface-4 animate-pulse"></div>
|
||||||
|
<div class="w-16 h-3 rounded-full bg-surface-4 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isEmpty">
|
||||||
|
<span class="text-red"> Error: Project has no members. This shouldn't happen. </span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="organization">
|
||||||
|
<AutoLink
|
||||||
|
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
|
||||||
|
:to="orgLink(organization.slug)"
|
||||||
|
:target="linkTarget ?? null"
|
||||||
|
>
|
||||||
|
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
||||||
|
<div class="flex flex-col flex-nowrap justify-center">
|
||||||
|
<span class="group-hover:underline font-medium">
|
||||||
|
{{ organization.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-normal text-secondary flex items-center gap-1"
|
||||||
|
><OrganizationIcon /> {{ formatMessage(messages.organization) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</AutoLink>
|
||||||
|
<hr v-if="sortedMembers.length > 0" class="w-full border-button-border my-0.5" />
|
||||||
|
</template>
|
||||||
<AutoLink
|
<AutoLink
|
||||||
|
v-for="member in sortedMembers"
|
||||||
|
:key="`member-${member.id}`"
|
||||||
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
|
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
|
||||||
:to="orgLink(organization.slug)"
|
:to="userLink(member.user.username)"
|
||||||
:target="linkTarget ?? null"
|
:target="resolveLinkTarget(userLinkTarget)"
|
||||||
>
|
>
|
||||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="32px" circle />
|
||||||
<div class="flex flex-col flex-nowrap justify-center">
|
<div class="flex flex-col">
|
||||||
<span class="group-hover:underline font-medium">
|
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
||||||
{{ organization.name }}
|
<span class="min-w-0 overflow-hidden truncate">{{ member.user.username }}</span>
|
||||||
|
<CrownIcon
|
||||||
|
v-if="member.is_owner"
|
||||||
|
v-tooltip="formatMessage(messages.owner)"
|
||||||
|
class="text-brand-orange"
|
||||||
|
/>
|
||||||
|
<ExternalIcon v-if="resolveLinkTarget(userLinkTarget) === '_blank'" />
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-normal text-secondary flex items-center gap-1"
|
<span class="text-sm font-normal text-secondary">{{ member.role }}</span>
|
||||||
><OrganizationIcon /> {{ formatMessage(messages.organization) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
<hr v-if="sortedMembers.length > 0" class="w-full border-button-border my-0.5" />
|
|
||||||
</template>
|
</template>
|
||||||
<AutoLink
|
|
||||||
v-for="member in sortedMembers"
|
|
||||||
:key="`member-${member.id}`"
|
|
||||||
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
|
|
||||||
:to="userLink(member.user.username)"
|
|
||||||
:target="resolveLinkTarget(userLinkTarget)"
|
|
||||||
>
|
|
||||||
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="32px" circle />
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
|
||||||
<span class="min-w-0 overflow-hidden truncate">{{ member.user.username }}</span>
|
|
||||||
<CrownIcon
|
|
||||||
v-if="member.is_owner"
|
|
||||||
v-tooltip="formatMessage(messages.owner)"
|
|
||||||
class="text-brand-orange"
|
|
||||||
/>
|
|
||||||
<ExternalIcon v-if="resolveLinkTarget(userLinkTarget) === '_blank'" />
|
|
||||||
</span>
|
|
||||||
<span class="text-sm font-normal text-secondary">{{ member.role }}</span>
|
|
||||||
</div>
|
|
||||||
</AutoLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -80,6 +94,7 @@ const props = defineProps<{
|
|||||||
userLink: (username: string) => string
|
userLink: (username: string) => string
|
||||||
linkTarget?: string
|
linkTarget?: string
|
||||||
userLinkTarget?: string | null
|
userLinkTarget?: string | null
|
||||||
|
loading?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function resolveLinkTarget(target: string | null | undefined): string | null {
|
function resolveLinkTarget(target: string | null | undefined): string | null {
|
||||||
@@ -113,6 +128,8 @@ const sortedMembers = computed(() => {
|
|||||||
return owner ? [owner, ...rest] : rest
|
return owner ? [owner, ...rest] : rest
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isEmpty = computed(() => !props.organization && sortedMembers.value.length === 0)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'project.about.creators.title',
|
id: 'project.about.creators.title',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="hasContent" class="flex flex-col gap-3">
|
<div v-if="loading || hasContent" class="flex flex-col gap-3">
|
||||||
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -15,6 +15,10 @@
|
|||||||
<CopyIcon class="shrink-0" />
|
<CopyIcon class="shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="loading && !projectV3"
|
||||||
|
class="h-12 rounded-2xl bg-surface-4 animate-pulse"
|
||||||
|
></div>
|
||||||
|
|
||||||
<section v-if="requiredContent" class="flex flex-col gap-2">
|
<section v-if="requiredContent" class="flex flex-col gap-2">
|
||||||
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.requiredContent) }}</h3>
|
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.requiredContent) }}</h3>
|
||||||
@@ -26,8 +30,13 @@
|
|||||||
:onclick-version="requiredContent.onclickVersion"
|
:onclick-version="requiredContent.onclickVersion"
|
||||||
:onclick-download="requiredContent.onclickDownload"
|
:onclick-download="requiredContent.onclickDownload"
|
||||||
:show-custom-modpack-tooltip="requiredContent.showCustomModpackTooltip"
|
:show-custom-modpack-tooltip="requiredContent.showCustomModpackTooltip"
|
||||||
|
:loading-version="loading"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-else-if="loading" class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.requiredContent) }}</h3>
|
||||||
|
<div class="h-[52px] rounded-2xl bg-surface-4 animate-pulse"></div>
|
||||||
|
</section>
|
||||||
<section v-if="recommendedVersions.length" class="flex flex-col gap-2">
|
<section v-if="recommendedVersions.length" class="flex flex-col gap-2">
|
||||||
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.minecraftJava) }}</h3>
|
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.minecraftJava) }}</h3>
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
@@ -57,6 +66,17 @@
|
|||||||
</TagItem>
|
</TagItem>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-else-if="loading" class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.minecraftJava) }}</h3>
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<div
|
||||||
|
v-for="width in ['w-16', 'w-20']"
|
||||||
|
:key="`version-skeleton-${width}`"
|
||||||
|
class="h-[26px] rounded-full bg-surface-4 animate-pulse"
|
||||||
|
:class="width"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section v-if="props.ping !== undefined || region" class="flex flex-col gap-2">
|
<section v-if="props.ping !== undefined || region" class="flex flex-col gap-2">
|
||||||
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.region) }}</h3>
|
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.region) }}</h3>
|
||||||
<div class="flex flex-wrap gap-1.5 items-center">
|
<div class="flex flex-wrap gap-1.5 items-center">
|
||||||
@@ -115,6 +135,7 @@ interface Props {
|
|||||||
loaders?: string[]
|
loaders?: string[]
|
||||||
ping?: number
|
ping?: number
|
||||||
statusOnline?: boolean
|
statusOnline?: boolean
|
||||||
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
>
|
>
|
||||||
{{ versionNumber }}
|
{{ versionNumber }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="loadingVersion"
|
||||||
|
class="w-16 h-4 my-0.5 rounded-full bg-surface-4 animate-pulse"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -58,6 +62,7 @@ defineProps<{
|
|||||||
onclickVersion?: () => void
|
onclickVersion?: () => void
|
||||||
onclickDownload?: () => void
|
onclickDownload?: () => void
|
||||||
showCustomModpackTooltip?: boolean
|
showCustomModpackTooltip?: boolean
|
||||||
|
loadingVersion?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|||||||
Reference in New Issue
Block a user