feat: fixed the collections page sorting logic and add URL persistence (#5375)
* "feat(collections): fix sorting logic and add URL persistence" * fix(navigation): use replaceState for project filters to prevent history pollution * Revert "fix(navigation): use replaceState for project filters to prevent history pollution" This reverts commit 3924855fafcf2921056e31b7606a143de01ed6a6. * fix: lint + devin --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -578,6 +578,18 @@
|
|||||||
"dashboard.collections.button.create-new": {
|
"dashboard.collections.button.create-new": {
|
||||||
"message": "Create new"
|
"message": "Create new"
|
||||||
},
|
},
|
||||||
|
"dashboard.collections.empty.get-started-hint": {
|
||||||
|
"message": "Create your first collection to get started!"
|
||||||
|
},
|
||||||
|
"dashboard.collections.empty.no-collections": {
|
||||||
|
"message": "You don't have any collections yet"
|
||||||
|
},
|
||||||
|
"dashboard.collections.empty.no-match": {
|
||||||
|
"message": "No collections match your search"
|
||||||
|
},
|
||||||
|
"dashboard.collections.empty.no-match-hint": {
|
||||||
|
"message": "Try adjusting your filters or search terms."
|
||||||
|
},
|
||||||
"dashboard.collections.label.projects-count": {
|
"dashboard.collections.label.projects-count": {
|
||||||
"message": "{count, plural, one {{count} project} other {{count} projects}}"
|
"message": "{count, plural, one {{count} project} other {{count} projects}}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,24 +2,48 @@
|
|||||||
<div class="universal-card">
|
<div class="universal-card">
|
||||||
<CollectionCreateModal ref="modal_creation" />
|
<CollectionCreateModal ref="modal_creation" />
|
||||||
<h2 class="text-2xl">{{ formatMessage(commonMessages.collectionsLabel) }}</h2>
|
<h2 class="text-2xl">{{ formatMessage(commonMessages.collectionsLabel) }}</h2>
|
||||||
<div class="search-row">
|
<div class="mb-3 flex flex-col gap-3">
|
||||||
<div class="flex-grow">
|
<label for="search-input" hidden>{{ formatMessage(messages.searchInputLabel) }}</label>
|
||||||
<label for="search-input" hidden>{{ formatMessage(messages.searchInputLabel) }}</label>
|
<StyledInput
|
||||||
<StyledInput
|
id="search-input"
|
||||||
id="search-input"
|
v-model="filterQuery"
|
||||||
v-model="filterQuery"
|
:icon="SearchIcon"
|
||||||
:icon="SearchIcon"
|
type="text"
|
||||||
type="text"
|
clearable
|
||||||
clearable
|
placeholder="Search collections..."
|
||||||
placeholder="Search collections..."
|
wrapper-class="w-full"
|
||||||
wrapper-class="w-full"
|
input-class="!h-12"
|
||||||
input-class="h-8"
|
/>
|
||||||
/>
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<DropdownSelect
|
||||||
|
v-slot="{ selected }"
|
||||||
|
v-model="sortBy"
|
||||||
|
class="!w-auto flex-grow md:flex-grow-0"
|
||||||
|
name="Sort by"
|
||||||
|
:options="['updated', 'created', 'name']"
|
||||||
|
:display-name="
|
||||||
|
(option) =>
|
||||||
|
option === 'updated'
|
||||||
|
? 'Recently Updated'
|
||||||
|
: option === 'created'
|
||||||
|
? 'Recently Created'
|
||||||
|
: 'Name (A-Z)'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
|
</DropdownSelect>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
class="ml-auto"
|
||||||
|
@click="(event) => $refs.modal_creation.show(event)"
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.createNewButton) }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" @click="(event) => $refs.modal_creation.show(event)">
|
|
||||||
<PlusIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(messages.createNewButton) }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collections-grid">
|
<div class="collections-grid">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
@@ -50,9 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-for="collection in orderedCollections.sort(
|
v-for="collection in orderedCollections"
|
||||||
(a, b) => new Date(b.created) - new Date(a.created),
|
|
||||||
)"
|
|
||||||
:key="collection.id"
|
:key="collection.id"
|
||||||
:to="`/collection/${collection.id}`"
|
:to="`/collection/${collection.id}`"
|
||||||
class="universal-card recessed collection"
|
class="universal-card recessed collection"
|
||||||
@@ -95,6 +117,25 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="orderedCollections.length === 0" class="empty-state-container">
|
||||||
|
<div class="py-12 text-center">
|
||||||
|
<BoxIcon class="mx-auto h-12 w-12 text-secondary opacity-50" aria-hidden="true" />
|
||||||
|
<p class="mt-4 text-lg font-medium text-contrast">
|
||||||
|
{{
|
||||||
|
filterQuery
|
||||||
|
? formatMessage(messages.emptyNoMatch)
|
||||||
|
: formatMessage(messages.emptyNoCollections)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
{{
|
||||||
|
filterQuery
|
||||||
|
? formatMessage(messages.emptyNoMatchHint)
|
||||||
|
: formatMessage(messages.emptyGetStartedHint)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +147,15 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
DropdownSelect,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
|
||||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||||
|
|
||||||
@@ -134,6 +183,22 @@ const messages = defineMessages({
|
|||||||
id: 'dashboard.collections.label.search-input',
|
id: 'dashboard.collections.label.search-input',
|
||||||
defaultMessage: 'Search your collections',
|
defaultMessage: 'Search your collections',
|
||||||
},
|
},
|
||||||
|
emptyNoMatch: {
|
||||||
|
id: 'dashboard.collections.empty.no-match',
|
||||||
|
defaultMessage: 'No collections match your search',
|
||||||
|
},
|
||||||
|
emptyNoCollections: {
|
||||||
|
id: 'dashboard.collections.empty.no-collections',
|
||||||
|
defaultMessage: "You don't have any collections yet",
|
||||||
|
},
|
||||||
|
emptyNoMatchHint: {
|
||||||
|
id: 'dashboard.collections.empty.no-match-hint',
|
||||||
|
defaultMessage: 'Try adjusting your filters or search terms.',
|
||||||
|
},
|
||||||
|
emptyGetStartedHint: {
|
||||||
|
id: 'dashboard.collections.empty.get-started-hint',
|
||||||
|
defaultMessage: 'Create your first collection to get started!',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -157,19 +222,33 @@ const { data: collections } = await useAsyncData(`user/${auth.value.user.id}/col
|
|||||||
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
|
useBaseFetch(`user/${auth.value.user.id}/collections`, { apiVersion: 3 }),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const route = useNativeRoute()
|
||||||
|
const router = useNativeRouter()
|
||||||
|
const validSortOptions = ['updated', 'created', 'name']
|
||||||
|
const sortBy = ref(validSortOptions.includes(route.query.s) ? route.query.s : 'updated')
|
||||||
|
|
||||||
const orderedCollections = computed(() => {
|
const orderedCollections = computed(() => {
|
||||||
if (!collections.value) return []
|
if (!collections.value) return []
|
||||||
return [...collections.value] // copy to avoid in-place mutation (no side effects)
|
return [...collections.value]
|
||||||
|
.filter(
|
||||||
|
(c) => !filterQuery.value || c.name.toLowerCase().includes(filterQuery.value.toLowerCase()),
|
||||||
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aUpdated = new Date(a.updated)
|
if (sortBy.value === 'name') return a.name.localeCompare(b.name)
|
||||||
const bUpdated = new Date(b.updated)
|
if (sortBy.value === 'created') return new Date(b.created) - new Date(a.created)
|
||||||
return bUpdated - aUpdated
|
return new Date(b.updated) - new Date(a.updated)
|
||||||
})
|
|
||||||
.filter((collection) => {
|
|
||||||
if (!filterQuery.value) return true
|
|
||||||
return collection.name.toLowerCase().includes(filterQuery.value.toLowerCase())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(sortBy, (newVal) => {
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
s: newVal,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.collections-grid {
|
.collections-grid {
|
||||||
@@ -225,21 +304,4 @@ const orderedCollections = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-row {
|
|
||||||
margin-bottom: var(--gap-lg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-lg) var(--gap-sm);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user