mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 21:17:06 +00:00
fix navtabs issues and unify project lists (#7245)
* fix navtabs issues and unify project lists * move page navtab logic to component
This commit is contained in:
@@ -221,144 +221,117 @@
|
||||
</UserPageHeader>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="navLinks.length > 2" class="max-w-full overflow-x-auto">
|
||||
<NavTabs :links="navLinks" replace />
|
||||
</div>
|
||||
<NavTabs v-if="navLinks.length > 2" :links="navLinks" replace page-nav />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<ProjectCardList
|
||||
v-if="selectedProjectType !== 'collection' && filteredProjects.length > 0"
|
||||
:layout="displayMode"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.id"
|
||||
:link="projectLink(project)"
|
||||
:title="project.title"
|
||||
:icon-url="project.icon_url"
|
||||
:date-updated="project.updated"
|
||||
:downloads="project.downloads"
|
||||
:summary="project.description"
|
||||
:tags="[...project.categories, ...project.loaders]"
|
||||
:all-tags="[
|
||||
...project.categories,
|
||||
...project.loaders,
|
||||
...project.additional_categories,
|
||||
]"
|
||||
:followers="project.followers"
|
||||
:banner="project.gallery?.find((image) => image.featured)?.url"
|
||||
:color="project.color"
|
||||
:environment="{
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}"
|
||||
:layout="displayMode === 'list' ? 'list' : 'grid'"
|
||||
:status="project.status"
|
||||
>
|
||||
<template v-if="$slots['project-actions']" #actions>
|
||||
<slot name="project-actions" :project="project" />
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</ProjectCardList>
|
||||
<div class="flex flex-col gap-3">
|
||||
<ProjectList
|
||||
v-if="selectedProjectType !== 'collection' && filteredProjects.length > 0"
|
||||
:projects="filteredProjects"
|
||||
:layout="displayMode"
|
||||
:link-mode="projectLinkMode"
|
||||
show-status
|
||||
>
|
||||
<template v-if="$slots['project-actions']" #actions="{ project }">
|
||||
<slot name="project-actions" :project="project" />
|
||||
</template>
|
||||
</ProjectList>
|
||||
|
||||
<EmptyState
|
||||
v-if="showProjectsEmptyState"
|
||||
type="empty"
|
||||
:heading="formatMessage(messages.profileNoProjectsLabel)"
|
||||
:description="
|
||||
isSelf ? formatMessage(messages.profileNoProjectsAuthDescription) : undefined
|
||||
"
|
||||
<EmptyState
|
||||
v-if="showProjectsEmptyState"
|
||||
type="empty"
|
||||
:heading="formatMessage(messages.profileNoProjectsLabel)"
|
||||
:description="
|
||||
isSelf ? formatMessage(messages.profileNoProjectsAuthDescription) : undefined
|
||||
"
|
||||
>
|
||||
<template v-if="isSelf" #actions>
|
||||
<Button type="colored" color="brand" native-type="button" @click="createProject">
|
||||
{{ formatMessage(messages.createProjectButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</EmptyState>
|
||||
|
||||
<ProjectCardList
|
||||
v-if="selectedProjectType === null || selectedProjectType === 'collection'"
|
||||
layout="grid"
|
||||
>
|
||||
<SmartClickable
|
||||
v-for="collection in sortedCollections"
|
||||
:key="collection.id"
|
||||
class="h-full w-full"
|
||||
>
|
||||
<template v-if="isSelf" #actions>
|
||||
<Button type="colored" color="brand" native-type="button" @click="createProject">
|
||||
{{ formatMessage(messages.createProjectButton) }}
|
||||
</Button>
|
||||
<template #clickable>
|
||||
<AutoLink
|
||||
:to="collectionLink(collection.id)"
|
||||
class="no-click-animation custom-focus-indicator rounded-xl no-outline"
|
||||
/>
|
||||
</template>
|
||||
</EmptyState>
|
||||
|
||||
<ProjectCardList
|
||||
v-if="selectedProjectType === null || selectedProjectType === 'collection'"
|
||||
layout="grid"
|
||||
>
|
||||
<SmartClickable
|
||||
v-for="collection in sortedCollections"
|
||||
:key="collection.id"
|
||||
class="h-full w-full"
|
||||
<div
|
||||
class="smart-clickable:outline-on-focus smart-clickable:highlight-on-hover flex h-full w-full flex-col gap-4 overflow-hidden rounded-2xl border-[1px] border-solid border-surface-4 bg-surface-3 p-4 text-left transition-all"
|
||||
>
|
||||
<template #clickable>
|
||||
<AutoLink
|
||||
:to="collectionLink(collection.id)"
|
||||
class="no-click-animation custom-focus-indicator rounded-xl no-outline"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="smart-clickable:outline-on-focus smart-clickable:highlight-on-hover flex h-full w-full flex-col gap-4 overflow-hidden rounded-2xl border-[1px] border-solid border-surface-4 bg-surface-3 p-4 text-left transition-all"
|
||||
>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-4">
|
||||
<Avatar :src="collection.icon_url" size="64px" no-shadow />
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<h2
|
||||
class="smart-clickable:underline-on-hover m-0 truncate text-lg font-semibold text-contrast"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</h2>
|
||||
<div class="flex items-center gap-1">
|
||||
<LibraryIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.collectionLabel) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow text-primary">
|
||||
{{ collection.description }}
|
||||
</div>
|
||||
<div class="mt-auto flex flex-wrap items-center gap-4">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-4">
|
||||
<Avatar :src="collection.icon_url" size="64px" no-shadow />
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<h2
|
||||
class="smart-clickable:underline-on-hover m-0 truncate text-lg font-semibold text-contrast"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</h2>
|
||||
<div class="flex items-center gap-1">
|
||||
<BoxIcon />
|
||||
{{
|
||||
formatMessage(messages.collectionProjectsCount, {
|
||||
count: collection.projects.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<template v-if="collection.status === 'listed'">
|
||||
<GlobeIcon />
|
||||
{{ formatMessage(commonMessages.publicLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'unlisted'">
|
||||
<LinkIcon />
|
||||
{{ formatMessage(commonMessages.unlistedLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'private'">
|
||||
<LockIcon />
|
||||
{{ formatMessage(commonMessages.privateLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'rejected'">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.rejectedLabel) }}
|
||||
</template>
|
||||
<LibraryIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.collectionLabel) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</ProjectCardList>
|
||||
<div class="grow text-primary">
|
||||
{{ collection.description }}
|
||||
</div>
|
||||
<div class="mt-auto flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<BoxIcon />
|
||||
{{
|
||||
formatMessage(messages.collectionProjectsCount, {
|
||||
count: collection.projects.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<template v-if="collection.status === 'listed'">
|
||||
<GlobeIcon />
|
||||
{{ formatMessage(commonMessages.publicLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'unlisted'">
|
||||
<LinkIcon />
|
||||
{{ formatMessage(commonMessages.unlistedLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'private'">
|
||||
<LockIcon />
|
||||
{{ formatMessage(commonMessages.privateLabel) }}
|
||||
</template>
|
||||
<template v-else-if="collection.status === 'rejected'">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.rejectedLabel) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</ProjectCardList>
|
||||
|
||||
<EmptyState
|
||||
v-if="showCollectionsEmptyState"
|
||||
type="empty"
|
||||
:heading="formatMessage(messages.profileNoCollectionsLabel)"
|
||||
:description="
|
||||
isSelf ? formatMessage(messages.profileNoCollectionsAuthDescription) : undefined
|
||||
"
|
||||
>
|
||||
<template v-if="isSelf" #actions>
|
||||
<Button type="colored" color="brand" native-type="button" @click="createCollection">
|
||||
{{ formatMessage(messages.createCollectionButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-if="showCollectionsEmptyState"
|
||||
type="empty"
|
||||
:heading="formatMessage(messages.profileNoCollectionsLabel)"
|
||||
:description="
|
||||
isSelf ? formatMessage(messages.profileNoCollectionsAuthDescription) : undefined
|
||||
"
|
||||
>
|
||||
<template v-if="isSelf" #actions>
|
||||
<Button type="colored" color="brand" native-type="button" @click="createCollection">
|
||||
{{ formatMessage(messages.createCollectionButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</div>
|
||||
|
||||
<template #sidebar>
|
||||
@@ -430,6 +403,7 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
getPrimaryProjectType,
|
||||
isModrinthUser as checkIsModrinthUser,
|
||||
isOfficialAccount as checkIsOfficialAccount,
|
||||
UserBadge,
|
||||
@@ -448,8 +422,8 @@ import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||
import SmartClickable from '#ui/components/base/SmartClickable.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import NormalPage from '#ui/components/page/NormalPage.vue'
|
||||
import ProjectCard from '#ui/components/project/card/ProjectCard.vue'
|
||||
import ProjectCardList from '#ui/components/project/ProjectCardList.vue'
|
||||
import ProjectList from '#ui/components/project/ProjectList.vue'
|
||||
import UserBadges from '#ui/components/user/UserBadges.vue'
|
||||
import UserPageHeader from '#ui/components/user/UserPageHeader.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables'
|
||||
@@ -458,27 +432,25 @@ import {
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
injectTags,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages, getProjectTypeTitleMessage, sortProjectTypes } from '#ui/utils'
|
||||
import {
|
||||
catalogProjectTypes,
|
||||
commonMessages,
|
||||
filterProjectsByType,
|
||||
getProjectTypeTitleMessage,
|
||||
parseProjectTypeRouteParam,
|
||||
sortProjectTypes,
|
||||
} from '#ui/utils'
|
||||
|
||||
import EditUserModal from './components/edit-user-modal.vue'
|
||||
import { blockedUsersQueryKey, injectUserProfile } from './providers'
|
||||
import {
|
||||
hasActivePride26Midas,
|
||||
hasPride26Badge,
|
||||
projectUserSorting,
|
||||
resolveProjectType,
|
||||
} from './utils'
|
||||
import { hasActivePride26Midas, hasPride26Badge, projectUserSorting } from './utils'
|
||||
|
||||
type DisplayMode = 'list' | 'grid' | 'gallery'
|
||||
type ModalRef = {
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}
|
||||
type ResolvedProject = Labrinth.Projects.v2.Project & {
|
||||
resolvedProjectType: string
|
||||
}
|
||||
type EarlyAdopterProjectType =
|
||||
| 'modpack'
|
||||
| 'resourcepack'
|
||||
@@ -517,7 +489,6 @@ const props = withDefaults(
|
||||
|
||||
const userProfile = injectUserProfile()
|
||||
const auth = injectAuth()
|
||||
const tags = injectTags(null)
|
||||
const pageContext = injectPageContext()
|
||||
const notificationManager = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
@@ -696,28 +667,33 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const canLoadProfile = computed(() => props.userId.length > 0)
|
||||
const userQuery = useQuery({
|
||||
queryKey: computed(() => ['user', props.userId]),
|
||||
queryFn: () => userProfile.getUser(props.userId),
|
||||
enabled: computed(() => Boolean(props.userId)),
|
||||
enabled: canLoadProfile,
|
||||
placeholderData: (previousData) => previousData,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: computed(() => ['user', props.userId, 'projects']),
|
||||
queryFn: () => userProfile.getProjects(props.userId),
|
||||
enabled: computed(() => Boolean(props.userId)),
|
||||
enabled: canLoadProfile,
|
||||
placeholderData: (previousData) => previousData,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const organizationsQuery = useQuery({
|
||||
queryKey: computed(() => ['user', props.userId, 'organizations']),
|
||||
queryFn: () => userProfile.getOrganizations(props.userId),
|
||||
enabled: computed(() => Boolean(props.userId)),
|
||||
enabled: canLoadProfile,
|
||||
placeholderData: (previousData) => previousData,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const collectionsQuery = useQuery({
|
||||
queryKey: computed(() => ['user', props.userId, 'collections']),
|
||||
queryFn: () => userProfile.getCollections(props.userId),
|
||||
enabled: computed(() => Boolean(props.userId)),
|
||||
enabled: canLoadProfile,
|
||||
placeholderData: (previousData) => previousData,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const blockedUsersQuery = useQuery({
|
||||
@@ -728,12 +704,7 @@ const blockedUsersQuery = useQuery({
|
||||
})
|
||||
|
||||
const user = computed(() => userQuery.data.value)
|
||||
const projects = computed<ResolvedProject[]>(() =>
|
||||
(projectsQuery.data.value ?? []).map((project) => ({
|
||||
...project,
|
||||
resolvedProjectType: resolveProjectType(project, tags?.loaders.value ?? []),
|
||||
})),
|
||||
)
|
||||
const projects = computed(() => projectsQuery.data.value ?? [])
|
||||
watch(
|
||||
() => projectsQuery.data.value,
|
||||
(projects) => {
|
||||
@@ -755,20 +726,11 @@ const isBlocked = computed(() =>
|
||||
user.value ? (blockedUsersQuery.data.value ?? []).includes(user.value.id) : false,
|
||||
)
|
||||
|
||||
const selectedProjectType = computed(() => {
|
||||
const projectType = props.projectType
|
||||
if (!projectType) return null
|
||||
if (projectType === 'collections' || projectType === 'collection') return 'collection'
|
||||
return projectType.endsWith('s') ? projectType.slice(0, -1) : projectType
|
||||
})
|
||||
const selectedProjectType = computed(() => parseProjectTypeRouteParam(props.projectType))
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
const selected = selectedProjectType.value
|
||||
return projects.value
|
||||
.filter((project) => !selected || project.resolvedProjectType === selected)
|
||||
.slice()
|
||||
.sort(projectUserSorting)
|
||||
})
|
||||
const filteredProjects = computed(() =>
|
||||
filterProjectsByType(projects.value, selectedProjectType.value).slice().sort(projectUserSorting),
|
||||
)
|
||||
|
||||
const sortedOrganizations = computed(() =>
|
||||
organizations.value.slice().sort((first, second) => first.name.localeCompare(second.name)),
|
||||
@@ -782,9 +744,8 @@ const sortedCollections = computed(() =>
|
||||
)
|
||||
|
||||
const projectTypes = computed(() => {
|
||||
const types = new Set(projects.value.map((project) => project.resolvedProjectType))
|
||||
if (collections.value.length > 0) types.add('collection')
|
||||
types.delete('project')
|
||||
const types = catalogProjectTypes(projects.value)
|
||||
if (collections.value.length > 0) types.push('collection')
|
||||
return sortProjectTypes(types)
|
||||
})
|
||||
|
||||
@@ -819,7 +780,7 @@ const profileHeaderSummary = computed(() => {
|
||||
const earliestProjectByType = computed(() => {
|
||||
const earliest = {} as Record<EarlyAdopterProjectType, Date>
|
||||
for (const project of projects.value) {
|
||||
const projectType = project.resolvedProjectType as EarlyAdopterProjectType
|
||||
const projectType = getPrimaryProjectType(project) as EarlyAdopterProjectType
|
||||
const published = new Date(project.published)
|
||||
if (!earliest[projectType] || published < earliest[projectType]) {
|
||||
earliest[projectType] = published
|
||||
@@ -881,13 +842,6 @@ function openPath(path: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function projectLink(project: ResolvedProject): string | (() => void) {
|
||||
if (props.projectLinkMode === 'app') {
|
||||
return `/project/${project.id}`
|
||||
}
|
||||
return `/${project.resolvedProjectType}/${project.slug || project.id}`
|
||||
}
|
||||
|
||||
function organizationLink(slug: string): string | (() => void) {
|
||||
return linkTarget(`/organization/${encodeURIComponent(slug)}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user