Skip to content

Commit 60abdf4

Browse files
committed
Begin collections implementation
1 parent cac44ef commit 60abdf4

File tree

18 files changed

+480
-59
lines changed

18 files changed

+480
-59
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<template>
2+
<Teleport to="#sidebar-teleport-target">
3+
<CollectionSidebarDescription v-if="collection" :collection="collection" class="project-sidebar-section" />
4+
<CollectionSidebarCurator v-if="curator" :user="curator" :link="`/user/${curator.id}`" class="project-sidebar-section" />
5+
<CollectionSidebarDetails v-if="collection" :collection="collection" class="project-sidebar-section" />
6+
</Teleport>
7+
<div v-if="collection" class="p-6 flex flex-col gap-4">
8+
<InstanceIndicator :instance="instance" />
9+
<CollectionHeader :collection="collection">
10+
<template #actions>
11+
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
12+
<OverflowMenu
13+
:options="[
14+
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },
15+
]"
16+
aria-label="More options"
17+
>
18+
<MoreVerticalIcon aria-hidden="true" />
19+
<template #copy-id>
20+
<ClipboardCopyIcon aria-hidden="true" />
21+
{{ formatMessage(commonMessages.copyIdButton) }}
22+
</template>
23+
</OverflowMenu>
24+
</ButtonStyled>
25+
</template>
26+
</CollectionHeader>
27+
<div v-if="projects">
28+
<ProjectsList :projects="projects" :project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`" :experimental-colors="themeStore.featureFlags.project_card_background">
29+
<template #project-actions="{ project }">
30+
<ProjectCardActions :instance="instance" :instance-content="instanceContent" :project="project" />
31+
</template>
32+
</ProjectsList>
33+
</div>
34+
</div>
35+
</template>
36+
37+
<script setup lang="ts">
38+
import { useRoute } from 'vue-router'
39+
import { ref, type Ref, watch } from 'vue'
40+
import { handleError } from '@/store/notifications.js'
41+
import {
42+
ProjectsList,
43+
ButtonStyled,
44+
commonMessages,
45+
OverflowMenu,
46+
CollectionHeader, CollectionSidebarCurator, CollectionSidebarDescription, CollectionSidebarDetails
47+
} from '@modrinth/ui'
48+
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
49+
import { useVIntl } from '@vintl/vintl'
50+
import { useFetch } from '@/helpers/fetch'
51+
import type { User, Project, Collection } from '@modrinth/utils'
52+
import { useBreadcrumbs } from '@/store/breadcrumbs'
53+
import { useTheming } from '@/store/theme'
54+
import { useInstanceContext } from '@/composables/instance-context'
55+
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
56+
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
57+
58+
const breadcrumbs = useBreadcrumbs()
59+
const route = useRoute()
60+
const { formatMessage } = useVIntl()
61+
62+
const collection: Ref<Collection | null> = ref(null)
63+
const curator: Ref<User | null> = ref(null)
64+
const projects: Ref<Project[]> = ref([])
65+
66+
async function fetchCollection() {
67+
collection.value = await useFetch(`https://api.modrinth.com/v3/collection/${route.params.id}`).catch(handleError)
68+
69+
if (!collection.value) {
70+
return;
71+
}
72+
73+
[ projects.value, curator.value ] = await Promise.all([
74+
useFetch(`https://api.modrinth.com/v2/projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`),
75+
useFetch(`https://api.modrinth.com/v2/user/${collection.value.user}`).catch(handleError),
76+
])
77+
78+
breadcrumbs.setContext({ name: 'Collection', link: `/collection/${collection.value.name}` })
79+
breadcrumbs.setName('Collection', collection.value.name)
80+
}
81+
82+
await fetchCollection()
83+
84+
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
85+
86+
watch(
87+
() => route.params.id,
88+
async () => {
89+
if (route.params.id && route.path.startsWith('/collection')) {
90+
await fetchCollection()
91+
}
92+
},
93+
)
94+
95+
const themeStore = useTheming()
96+
97+
98+
async function copyId() {
99+
if (collection.value) {
100+
await navigator.clipboard.writeText(String(collection.value.id));
101+
}
102+
}
103+
</script>
104+
<style scoped lang="scss">
105+
.project-sidebar-section {
106+
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
107+
}
108+
.project-sidebar-section:not(:last-child) {
109+
@apply border-b-[1px];
110+
}
111+
</style>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Index from './Index.vue'
2+
3+
export { Index }

apps/app-frontend/src/pages/organization/Index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<InstanceIndicator :instance="instance" />
77
<OrganizationHeader :organization="organization" :download-count="sumDownloads" :project-count="projects.length">
88
<template #actions>
9-
<ButtonStyled circular type="transparent" size="large">
9+
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
1010
<OverflowMenu
1111
:options="[
1212
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },

apps/app-frontend/src/pages/user/Index.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
:download-count="sumDownloads"
88
class="project-sidebar-section"
99
/>
10+
<UserSidebarCollections :collections="collections" :link="(collection: Collection) => `/collection/${collection.id}${instanceQueryAppendage}`" class="project-sidebar-section" />
1011
</Teleport>
1112
<div v-if="user" class="p-6 flex flex-col gap-4">
1213
<InstanceIndicator :instance="instance" />
@@ -54,12 +55,12 @@ import {
5455
commonMessages,
5556
OverflowMenu,
5657
UserHeader,
57-
UserSidebarBadges
58+
UserSidebarBadges, UserSidebarCollections
5859
} from '@modrinth/ui'
5960
import { ReportIcon, ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
6061
import { useVIntl } from '@vintl/vintl'
6162
import { useFetch } from '@/helpers/fetch'
62-
import type { User, Project, Organization } from '@modrinth/utils'
63+
import type { User, Project, Organization, Collection } from '@modrinth/utils'
6364
import { useBreadcrumbs } from '@/store/breadcrumbs'
6465
import { useTheming } from '@/store/theme'
6566
import { useInstanceContext } from '@/composables/instance-context'
@@ -73,11 +74,15 @@ const { formatMessage } = useVIntl()
7374
const user: Ref<User | null> = ref(null)
7475
const projects: Ref<Project[]> = ref([])
7576
const organizations: Ref<Organization[]> = ref([])
77+
const collections: Ref<Collection[]> = ref([])
7678
7779
async function fetchUser() {
78-
user.value = await useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError)
79-
projects.value = await useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError)
80-
organizations.value = await useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(handleError)
80+
[ user.value, projects.value, organizations.value, collections.value ] = await Promise.all([
81+
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError),
82+
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError),
83+
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(handleError),
84+
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/collections`).catch(handleError)
85+
])
8186
8287
if (!user.value) {
8388
return;

apps/app-frontend/src/routes.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Pages from '@/pages'
33
import * as Project from '@/pages/project'
44
import * as User from '@/pages/user'
55
import * as Organization from '@/pages/organization'
6+
import * as Collection from '@/pages/collection'
67
import * as Instance from '@/pages/instance'
78
import * as Library from '@/pages/library'
89

@@ -122,6 +123,16 @@ export default new createRouter({
122123
breadcrumb: [{ name: '?Organization', link: '/organization/{id}' }],
123124
},
124125
},
126+
{
127+
path: '/collection/:id',
128+
name: 'Collection',
129+
component: Collection.Index,
130+
props: true,
131+
meta: {
132+
useContext: true,
133+
breadcrumb: [{ name: '?Collection', link: '/collection/{id}' }],
134+
},
135+
},
125136
{
126137
path: '/instance/:id',
127138
name: 'Instance',

packages/ui/src/components/base/AutoLink.vue

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22
<span v-if="to === undefined" v-bind="$attrs">
33
<slot />
44
</span>
5-
<router-link v-else-if="link.type === 'router'" :to="link.destination" v-bind="$attrs" class="has-link has-router-link" @click="link.callback">
5+
<router-link
6+
v-else-if="link.type === 'router'"
7+
:to="link.destination"
8+
v-bind="$attrs"
9+
class="has-link has-router-link"
10+
@click="link.callback"
11+
>
612
<slot />
713
</router-link>
8-
<a v-else-if="link.type === 'external'" :href="link.destination" v-bind="$attrs" class="has-link has-external-link" @click="link.callback">
14+
<a
15+
v-else-if="link.type === 'external'"
16+
:href="link.destination"
17+
v-bind="$attrs"
18+
class="has-link has-external-link"
19+
@click="link.callback"
20+
>
921
<slot />
1022
</a>
1123
</template>
1224

1325
<script setup lang="ts">
1426
import { computed, type Ref } from 'vue'
15-
import {
16-
asLink,
17-
type Link,
18-
type Linkish
19-
} from '../../utils/link'
27+
import { asLink, type Link, type Linkish } from '../../utils/link'
2028
2129
const props = defineProps<{
2230
to?: Linkish
@@ -26,9 +34,5 @@ defineOptions({
2634
inheritAttrs: false,
2735
})
2836
29-
const link: Ref<Link> = computed(() => {
30-
const linkTest = asLink(props.to as Link)
31-
console.log(linkTest)
32-
return linkTest
33-
})
37+
const link: Ref<Link> = computed(() => asLink(props.to as Link))
3438
</script>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<ContentPageHeader>
3+
<template #icon>
4+
<Avatar :src="collection.icon_url" :alt="collection.name" size="64px" />
5+
</template>
6+
<template #title>
7+
{{ collection.name }}
8+
</template>
9+
<template #stats>
10+
<div
11+
v-if="collection.projects.length > 0"
12+
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
13+
>
14+
<CollectionStatusBadge :status="collection.status" />
15+
</div>
16+
<div
17+
v-if="collection.projects.length > 0"
18+
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
19+
>
20+
<BoxIcon class="h-5 w-5 text-secondary" />
21+
{{ formatMessage(commonMessages.projectsStat, { count: formatNumber(collection.projects.length, false) }) }}
22+
23+
</div>
24+
<div
25+
v-tooltip="
26+
formatMessage(commonMessages.dateAtTimeTooltip, {
27+
date: new Date(collection.updated),
28+
time: new Date(collection.updated),
29+
})
30+
"
31+
class="flex items-center gap-2 font-semibold"
32+
>
33+
<HistoryIcon class="h-5 w-5 text-secondary" />
34+
{{ formatMessage(commonMessages.updatedDate, { date: dayjs(collection.updated).fromNow() }) }}
35+
</div>
36+
</template>
37+
<template #actions>
38+
<slot name="actions" />
39+
</template>
40+
</ContentPageHeader>
41+
</template>
42+
<script setup lang="ts">
43+
import { GlobeIcon, BoxIcon, HistoryIcon } from '@modrinth/assets'
44+
import Avatar from '../base/Avatar.vue'
45+
import ContentPageHeader from '../base/ContentPageHeader.vue'
46+
import { type Collection, formatNumber } from '@modrinth/utils'
47+
import { commonMessages } from '../../utils/common-messages'
48+
import dayjs from 'dayjs'
49+
import { useVIntl } from '@vintl/vintl'
50+
import ProjectStatusBadge from '../project/ProjectStatusBadge.vue'
51+
import CollectionStatusBadge from './CollectionStatusBadge.vue'
52+
53+
const { formatMessage } = useVIntl()
54+
55+
defineProps<{
56+
collection: Collection
57+
}>()
58+
</script>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<template>
2+
<div class="flex flex-col gap-3">
3+
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
4+
<div class="flex flex-col gap-3 font-semibold">
5+
<AutoLink
6+
class="flex gap-2 items-center w-fit text-primary leading-[1.2] group"
7+
:to="linkifiedLink"
8+
>
9+
<Avatar :src="user.avatar_url" :alt="user.username" size="32px" circle />
10+
<div class="flex flex-col">
11+
<span class="flex flex-row flex-nowrap items-center gap-1 group-hover:underline">
12+
{{ user.username }}
13+
<ExternalIcon v-if="linkifiedLink.type === 'external'" />
14+
</span>
15+
</div>
16+
</AutoLink>
17+
</div>
18+
</div>
19+
</template>
20+
<script setup lang="ts">
21+
import { ExternalIcon } from '@modrinth/assets'
22+
import { useVIntl, defineMessages } from '@vintl/vintl'
23+
import Avatar from '../base/Avatar.vue'
24+
import AutoLink from '../base/AutoLink.vue'
25+
import type { User } from '@modrinth/utils'
26+
import { asLink, type Linkish } from '../../utils/link'
27+
import { computed } from 'vue'
28+
29+
const { formatMessage } = useVIntl()
30+
31+
const props = defineProps<{
32+
user: User
33+
link: Linkish
34+
}>()
35+
36+
const messages = defineMessages({
37+
title: {
38+
id: 'collection.curator.title',
39+
defaultMessage: 'Curator',
40+
},
41+
})
42+
43+
const linkifiedLink = computed(() => asLink(props.link))
44+
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<template>
2+
<div class="flex flex-col gap-3">
3+
<h2 class="text-lg m-0">{{ formatMessage(messages.title) }}</h2>
4+
<div>
5+
{{ collection.description }}
6+
</div>
7+
</div>
8+
</template>
9+
<script setup lang="ts">
10+
import { useVIntl, defineMessages } from '@vintl/vintl'
11+
import type { Collection } from '@modrinth/utils'
12+
13+
const { formatMessage } = useVIntl()
14+
15+
defineProps<{
16+
collection: Collection
17+
}>()
18+
19+
const messages = defineMessages({
20+
title: {
21+
id: 'collection.description.title',
22+
defaultMessage: 'Description',
23+
},
24+
})
25+
</script>

0 commit comments

Comments
 (0)