Begin collections implementation
This commit is contained in:
1 parent
54f408dc6c
commit
adf213f32a
17 files changed
+463
-42
No files matched your search
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<CollectionSidebarDescription v-if="collection" :collection="collection" class="project-sidebar-section" />
|
||||
<CollectionSidebarCurator v-if="curator" :user="curator" :link="`/user/${curator.id}`" class="project-sidebar-section" />
|
||||
<CollectionSidebarDetails v-if="collection" :collection="collection" class="project-sidebar-section" />
|
||||
</Teleport>
|
||||
<div v-if="collection" class="p-6 flex flex-col gap-4">
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<CollectionHeader :collection="collection">
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</CollectionHeader>
|
||||
<div v-if="projects">
|
||||
<ProjectsList :projects="projects" :project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`" :experimental-colors="themeStore.featureFlags.project_card_background">
|
||||
<template #project-actions="{ project }">
|
||||
<ProjectCardActions :instance="instance" :instance-content="instanceContent" :project="project" />
|
||||
</template>
|
||||
</ProjectsList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, type Ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import {
|
||||
ProjectsList,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
OverflowMenu,
|
||||
CollectionHeader, CollectionSidebarCurator, CollectionSidebarDescription, CollectionSidebarDetails
|
||||
} from '@modrinth/ui'
|
||||
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { useFetch } from '@/helpers/fetch'
|
||||
import type { User, Project, Collection } from '@modrinth/utils'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { useInstanceContext } from '@/composables/instance-context'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const route = useRoute()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const collection: Ref<Collection | null> = ref(null)
|
||||
const curator: Ref<User | null> = ref(null)
|
||||
const projects: Ref<Project[]> = ref([])
|
||||
|
||||
async function fetchCollection() {
|
||||
collection.value = await useFetch(`https://api.modrinth.com/v3/collection/${route.params.id}`).catch(handleError)
|
||||
|
||||
if (!collection.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
[ projects.value, curator.value ] = await Promise.all([
|
||||
useFetch(`https://api.modrinth.com/v2/projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`),
|
||||
useFetch(`https://api.modrinth.com/v2/user/${collection.value.user}`).catch(handleError),
|
||||
])
|
||||
|
||||
breadcrumbs.setContext({ name: 'Collection', link: `/collection/${collection.value.name}` })
|
||||
breadcrumbs.setName('Collection', collection.value.name)
|
||||
}
|
||||
|
||||
await fetchCollection()
|
||||
|
||||
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/collection')) {
|
||||
await fetchCollection()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
async function copyId() {
|
||||
if (collection.value) {
|
||||
await navigator.clipboard.writeText(String(collection.value.id));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.project-sidebar-section {
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
|
||||
}
|
||||
.project-sidebar-section:not(:last-child) {
|
||||
@apply border-b-[1px];
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import Index from './Index.vue'
|
||||
|
||||
export { Index }
|
||||
@@ -6,7 +6,7 @@
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<OrganizationHeader :organization="organization" :download-count="sumDownloads" :project-count="projects.length">
|
||||
<template #actions>
|
||||
<ButtonStyled circular type="transparent" size="large">
|
||||
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:download-count="sumDownloads"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<UserSidebarCollections :collections="collections" :link="(collection: Collection) => `/collection/${collection.id}${instanceQueryAppendage}`" class="project-sidebar-section" />
|
||||
</Teleport>
|
||||
<div v-if="user" class="p-6 flex flex-col gap-4">
|
||||
<InstanceIndicator :instance="instance" />
|
||||
@@ -54,12 +55,12 @@ import {
|
||||
commonMessages,
|
||||
OverflowMenu,
|
||||
UserHeader,
|
||||
UserSidebarBadges
|
||||
UserSidebarBadges, UserSidebarCollections
|
||||
} from '@modrinth/ui'
|
||||
import { ReportIcon, ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { useFetch } from '@/helpers/fetch'
|
||||
import type { User, Project, Organization } from '@modrinth/utils'
|
||||
import type { User, Project, Organization, Collection } from '@modrinth/utils'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { useInstanceContext } from '@/composables/instance-context'
|
||||
@@ -73,11 +74,15 @@ const { formatMessage } = useVIntl()
|
||||
const user: Ref<User | null> = ref(null)
|
||||
const projects: Ref<Project[]> = ref([])
|
||||
const organizations: Ref<Organization[]> = ref([])
|
||||
const collections: Ref<Collection[]> = ref([])
|
||||
|
||||
async function fetchUser() {
|
||||
user.value = await useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError)
|
||||
projects.value = await useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError)
|
||||
organizations.value = await useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(handleError)
|
||||
[ user.value, projects.value, organizations.value, collections.value ] = await Promise.all([
|
||||
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError),
|
||||
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError),
|
||||
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(handleError),
|
||||
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/collections`).catch(handleError)
|
||||
])
|
||||
|
||||
if (!user.value) {
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as Pages from '@/pages'
|
||||
import * as Project from '@/pages/project'
|
||||
import * as User from '@/pages/user'
|
||||
import * as Organization from '@/pages/organization'
|
||||
import * as Collection from '@/pages/collection'
|
||||
import * as Instance from '@/pages/instance'
|
||||
import * as Library from '@/pages/library'
|
||||
|
||||
@@ -122,6 +123,16 @@ export default new createRouter({
|
||||
breadcrumb: [{ name: '?Organization', link: '/organization/{id}' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/collection/:id',
|
||||
name: 'Collection',
|
||||
component: Collection.Index,
|
||||
props: true,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [{ name: '?Collection', link: '/collection/{id}' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/instance/:id',
|
||||
name: 'Instance',
|
||||
|
||||
@@ -2,21 +2,29 @@
|
||||
<span v-if="to === undefined" v-bind="$attrs">
|
||||
<slot />
|
||||
</span>
|
||||
<router-link v-else-if="link.type === 'router'" :to="link.destination" v-bind="$attrs" class="has-link has-router-link" @click="link.callback">
|
||||
<router-link
|
||||
v-else-if="link.type === 'router'"
|
||||
:to="link.destination"
|
||||
v-bind="$attrs"
|
||||
class="has-link has-router-link"
|
||||
@click="link.callback"
|
||||
>
|
||||
<slot />
|
||||
</router-link>
|
||||
<a v-else-if="link.type === 'external'" :href="link.destination" v-bind="$attrs" class="has-link has-external-link" @click="link.callback">
|
||||
<a
|
||||
v-else-if="link.type === 'external'"
|
||||
:href="link.destination"
|
||||
v-bind="$attrs"
|
||||
class="has-link has-external-link"
|
||||
@click="link.callback"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Ref } from 'vue'
|
||||
import {
|
||||
asLink,
|
||||
type Link,
|
||||
type Linkish
|
||||
} from '../../utils/link'
|
||||
import { asLink, type Link, type Linkish } from '../../utils/link'
|
||||
|
||||
const props = defineProps<{
|
||||
to?: Linkish
|
||||
@@ -26,9 +34,5 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const link: Ref<Link> = computed(() => {
|
||||
const linkTest = asLink(props.to as Link)
|
||||
console.log(linkTest)
|
||||
return linkTest
|
||||
})
|
||||
const link: Ref<Link> = computed(() => asLink(props.to as Link))
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="collection.icon_url" :alt="collection.name" size="64px" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ collection.name }}
|
||||
</template>
|
||||
<template #stats>
|
||||
<div
|
||||
v-if="collection.projects.length > 0"
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<CollectionStatusBadge :status="collection.status" />
|
||||
</div>
|
||||
<div
|
||||
v-if="collection.projects.length > 0"
|
||||
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
|
||||
>
|
||||
<BoxIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatMessage(commonMessages.projectsStat, { count: formatNumber(collection.projects.length, false) }) }}
|
||||
|
||||
</div>
|
||||
<div
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(collection.updated),
|
||||
time: new Date(collection.updated),
|
||||
})
|
||||
"
|
||||
class="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<HistoryIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatMessage(commonMessages.updatedDate, { date: dayjs(collection.updated).fromNow() }) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { GlobeIcon, BoxIcon, HistoryIcon } from '@modrinth/assets'
|
||||
import Avatar from '../base/Avatar.vue'
|
||||
import ContentPageHeader from '../base/ContentPageHeader.vue'
|
||||
import { type Collection, formatNumber } from '@modrinth/utils'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
import dayjs from 'dayjs'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import ProjectStatusBadge from '../project/ProjectStatusBadge.vue'
|
||||
import CollectionStatusBadge from './CollectionStatusBadge.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
defineProps<{
|
||||
collection: Collection
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
||||
<div class="flex flex-col gap-3 font-semibold">
|
||||
<AutoLink
|
||||
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
|
||||
:to="linkifiedLink"
|
||||
>
|
||||
<Avatar :src="user.avatar_url" :alt="user.username" size="32px" circle />
|
||||
<div class="flex flex-col">
|
||||
<span class="flex flex-row flex-nowrap items-center gap-1 group-hover:underline">
|
||||
{{ user.username }}
|
||||
<ExternalIcon v-if="linkifiedLink.type === 'external'" />
|
||||
</span>
|
||||
</div>
|
||||
</AutoLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import Avatar from '../base/Avatar.vue'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
import type { User } from '@modrinth/utils'
|
||||
import { asLink, type Linkish } from '../../utils/link'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
link: Linkish
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'collection.curator.title',
|
||||
defaultMessage: 'Curator',
|
||||
},
|
||||
})
|
||||
|
||||
const linkifiedLink = computed(() => asLink(props.link))
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
||||
<div>
|
||||
{{ collection.description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import type { Collection } from '@modrinth/utils'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
defineProps<{
|
||||
collection: Collection
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'collection.description.title',
|
||||
defaultMessage: 'Description',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
||||
<div class="flex flex-col gap-3 font-semibold [&>div]:flex [&>div]:gap-2 [&>div]:items-center">
|
||||
<div
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(collection.created),
|
||||
time: new Date(collection.created),
|
||||
})
|
||||
">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(commonMessages.createdAgoLabel, { ago: dayjs(collection.created).fromNow() }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CalendarIcon } from '@modrinth/assets'
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Collection } from '@modrinth/utils'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
defineProps<{
|
||||
collection: Collection
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'collection.details.title',
|
||||
defaultMessage: 'Details',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<Badge :icon="metadata.icon" :formatted-name="metadata.formattedName" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LockIcon,
|
||||
GlobeIcon,
|
||||
LinkIcon,
|
||||
UnknownIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useVIntl, type MessageDescriptor } from '@vintl/vintl'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import Badge from '../base/SimpleBadge.vue'
|
||||
import type { CollectionStatus } from '@modrinth/utils'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
|
||||
const props = defineProps<{
|
||||
status: CollectionStatus
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const metadata = computed(() => ({
|
||||
icon: statusMetadata[props.status]?.icon ?? statusMetadata.unknown.icon,
|
||||
formattedName: formatMessage(statusMetadata[props.status]?.message ?? props.status),
|
||||
}))
|
||||
|
||||
const statusMetadata: Record<CollectionStatus, { icon?: Component; message: MessageDescriptor }> = {
|
||||
listed: {
|
||||
icon: GlobeIcon,
|
||||
message: commonMessages.publicLabel
|
||||
},
|
||||
unlisted: {
|
||||
icon: LinkIcon,
|
||||
message: commonMessages.unlistedLabel
|
||||
},
|
||||
private: {
|
||||
icon: LockIcon,
|
||||
message: commonMessages.privateLabel
|
||||
},
|
||||
unknown: {
|
||||
icon: UnknownIcon,
|
||||
message: commonMessages.unknownLabel
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -104,8 +104,16 @@ export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
||||
export { default as UserBadge } from './user/UserBadge.vue'
|
||||
export { default as UserHeader } from './user/UserHeader.vue'
|
||||
export { default as UserSidebarBadges } from './user/UserSidebarBadges.vue'
|
||||
export { default as UserSidebarCollections } from './user/UserSidebarCollections.vue'
|
||||
export { default as UserSidebarOrganizations } from './user/UserSidebarOrganizations.vue'
|
||||
|
||||
// Organization
|
||||
export { default as OrganizationHeader } from './organization/OrganizationHeader.vue'
|
||||
export { default as OrganizationSidebarMembers } from './organization/OrganizationSidebarMembers.vue'
|
||||
|
||||
// Collection
|
||||
export { default as CollectionHeader } from './collection/CollectionHeader.vue'
|
||||
export { default as CollectionSidebarCurator } from './collection/CollectionSidebarCurator.vue'
|
||||
export { default as CollectionSidebarDescription } from './collection/CollectionSidebarDescription.vue'
|
||||
export { default as CollectionSidebarDetails } from './collection/CollectionSidebarDetails.vue'
|
||||
export { default as CollectionStatusBadge } from './collection/CollectionStatusBadge.vue'
|
||||
@@ -19,6 +19,7 @@ import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import Badge from '../base/SimpleBadge.vue'
|
||||
import type { ProjectStatus } from '@modrinth/utils'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
|
||||
const props = defineProps<{
|
||||
status: ProjectStatus
|
||||
@@ -34,17 +35,11 @@ const metadata = computed(() => ({
|
||||
const statusMetadata: Record<ProjectStatus, { icon?: Component; message: MessageDescriptor }> = {
|
||||
approved: {
|
||||
icon: GlobeIcon,
|
||||
message: defineMessage({
|
||||
id: 'project.visibility.public',
|
||||
defaultMessage: 'Public',
|
||||
}),
|
||||
message: commonMessages.publicLabel,
|
||||
},
|
||||
unlisted: {
|
||||
icon: LinkIcon,
|
||||
message: defineMessage({
|
||||
id: 'project.visibility.unlisted',
|
||||
defaultMessage: 'Unlisted',
|
||||
}),
|
||||
message: commonMessages.unlistedLabel,
|
||||
},
|
||||
withheld: {
|
||||
icon: LinkIcon,
|
||||
@@ -55,10 +50,7 @@ const statusMetadata: Record<ProjectStatus, { icon?: Component; message: Message
|
||||
},
|
||||
private: {
|
||||
icon: LockIcon,
|
||||
message: defineMessage({
|
||||
id: 'project.visibility.private',
|
||||
defaultMessage: 'Private',
|
||||
}),
|
||||
message: commonMessages.privateLabel,
|
||||
},
|
||||
scheduled: {
|
||||
icon: CalendarIcon,
|
||||
@@ -83,10 +75,7 @@ const statusMetadata: Record<ProjectStatus, { icon?: Component; message: Message
|
||||
},
|
||||
rejected: {
|
||||
icon: XIcon,
|
||||
message: defineMessage({
|
||||
id: 'project.visibility.rejected',
|
||||
defaultMessage: 'Rejected',
|
||||
}),
|
||||
message: commonMessages.rejectedLabel,
|
||||
},
|
||||
processing: {
|
||||
icon: UpdatedIcon,
|
||||
@@ -97,10 +86,7 @@ const statusMetadata: Record<ProjectStatus, { icon?: Component; message: Message
|
||||
},
|
||||
unknown: {
|
||||
icon: UnknownIcon,
|
||||
message: defineMessage({
|
||||
id: 'project.visibility.unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
}),
|
||||
message: commonMessages.unknownLabel,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<template v-if="projects.length > 0">
|
||||
|
||||
<div class="mb-3">
|
||||
<NavTabs
|
||||
v-if="availableProjectTypes.length > 1"
|
||||
:tabs="[
|
||||
{
|
||||
label: formatMessage(ANY_PROJECT_TYPE_MESSAGE),
|
||||
@@ -19,7 +18,6 @@
|
||||
]"
|
||||
class="mb-3"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 flex gap-2">
|
||||
<div class="iconified-input flex-1">
|
||||
<SearchIcon />
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div v-if="collections.length > 0" class="flex flex-col gap-3">
|
||||
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
|
||||
<div class="flex flex-col gap-2 place-items-center">
|
||||
<Tooltip
|
||||
v-for="collection in collectionsSorted"
|
||||
:key="`user-collection-${collection.id}`"
|
||||
class="w-full"
|
||||
placement="right"
|
||||
>
|
||||
<AutoLink
|
||||
:to="link(collection)"
|
||||
class="flex gap-2 group overflow-hidden"
|
||||
>
|
||||
<Avatar :src="collection.icon_url" :alt="'Icon for ' + collection.name" size="3rem" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="items-center text-primary font-bold gap-2 line-clamp-1 text-ellipsis group-hover:underline">
|
||||
{{ collection.name }}
|
||||
</span>
|
||||
<span class="text-secondary font-medium flex items-center gap-1">
|
||||
<BoxIcon /> {{ formatMessage(commonMessages.projectsStat, { count: formatNumber(collection.projects.length, false) }) }}
|
||||
</span>
|
||||
</div>
|
||||
</AutoLink>
|
||||
<template #popper>
|
||||
<div class="flex flex-col p-1 dark">
|
||||
<p class="text-sm font-bold text-contrast m-0 max-w-80">{{ collection.name }}</p>
|
||||
<p class="text-sm text-primary font-semibold m-0 max-w-80">{{ collection.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import { computed } from 'vue'
|
||||
import Avatar from '../base/Avatar.vue'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
import type { Collection } from '@modrinth/utils'
|
||||
import type { Linkish } from '../../utils/link'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import { BoxIcon } from '@modrinth/assets'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
collections: Collection[],
|
||||
link: (collection: Collection) => Linkish
|
||||
}>()
|
||||
|
||||
const collectionsSorted = computed(() => props.collections.slice().sort((a, b) => a.name.localeCompare(b.name)))
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'user.collections.title',
|
||||
defaultMessage: 'Collections',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -110,7 +110,7 @@ export const commonMessages = defineMessages({
|
||||
defaultMessage: '{count} {count, plural, one {member} other {members}}',
|
||||
},
|
||||
privateLabel: {
|
||||
id: 'collection.label.private',
|
||||
id: 'label.private',
|
||||
defaultMessage: 'Private',
|
||||
},
|
||||
publicLabel: {
|
||||
@@ -189,6 +189,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.unlisted',
|
||||
defaultMessage: 'Unlisted',
|
||||
},
|
||||
unknownLabel: {
|
||||
id: 'label.unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
},
|
||||
uploadImageButton: {
|
||||
id: 'button.upload-image',
|
||||
defaultMessage: 'Upload image',
|
||||
|
||||
@@ -335,3 +335,18 @@ export type OrganizationMember = {
|
||||
payouts_split: number
|
||||
ordering: number
|
||||
}
|
||||
|
||||
export type Collection = {
|
||||
id: ModrinthId
|
||||
user: User
|
||||
name: string
|
||||
description: string
|
||||
icon_url: string
|
||||
color: number
|
||||
status: CollectionStatus
|
||||
created: string
|
||||
updated: string
|
||||
projects: ModrinthId[]
|
||||
}
|
||||
|
||||
export type CollectionStatus = 'listed' | 'unlisted' | 'private' | 'unknown'
|
||||
Reference in new issue
Block a user