From ef8093f089a6114e95c45b2927c289008f53ae5a Mon Sep 17 00:00:00 2001 From: Brian Kasper Date: Sun, 1 Feb 2026 18:13:32 -0500 Subject: [PATCH 1/4] fix: initial skill sorting --- convex/skills.ts | 40 +++++++++++++++++++++++++++---------- src/routes/skills/index.tsx | 2 +- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/convex/skills.ts b/convex/skills.ts index be38017..8a2efcf 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -720,23 +720,43 @@ export const listPublicPage = query({ export const listPublicPageV2 = query({ args: { paginationOpts: paginationOptsValidator, + sort: v.optional( + v.union( + v.literal('newest'), + v.literal('updated'), + v.literal('downloads'), + v.literal('installs'), + v.literal('stars'), + v.literal('name'), + ), + ), + dir: v.optional(v.union(v.literal('asc'), v.literal('desc'))), }, handler: async (ctx, args) => { - // Use the new index to filter out soft-deleted skills at query time. - // softDeletedAt === undefined means active (non-deleted) skills only. + const sort = args.sort ?? 'updated' + const dir = args.dir ?? 'desc' + + const indexName = + sort === 'downloads' + ? 'by_stats_downloads' + : sort === 'stars' + ? 'by_stats_stars' + : sort === 'installs' + ? 'by_stats_installs_all_time' + : 'by_active_updated' + + const useActiveFilter = indexName === 'by_active_updated' + const result = await paginator(ctx.db, schema) .query('skills') - .withIndex('by_active_updated', (q) => q.eq('softDeletedAt', undefined)) - .order('desc') + .withIndex(indexName, useActiveFilter ? (q) => q.eq('softDeletedAt', undefined) : (q) => q) + .order(dir) .paginate(args.paginationOpts) - // Build the public skill entries (fetch latestVersion + ownerHandle) - const items = await buildPublicSkillEntries(ctx, result.page) + const page = useActiveFilter ? result.page : result.page.filter((s) => !s.softDeletedAt) + const items = await buildPublicSkillEntries(ctx, page) - return { - ...result, - page: items, - } + return { ...result, page: items } }, }) diff --git a/src/routes/skills/index.tsx b/src/routes/skills/index.tsx index bf0a2a6..6e7049c 100644 --- a/src/routes/skills/index.tsx +++ b/src/routes/skills/index.tsx @@ -84,7 +84,7 @@ export function SkillsIndex() { results: paginatedResults, status: paginationStatus, loadMore: loadMorePaginated, - } = usePaginatedQuery(api.skills.listPublicPageV2, hasQuery ? 'skip' : {}, { + } = usePaginatedQuery(api.skills.listPublicPageV2, hasQuery ? 'skip' : { sort, dir }, { initialNumItems: pageSize, }) From fee477051ceffb35b10c1c378364a4af18c345f4 Mon Sep 17 00:00:00 2001 From: Brian Kasper Date: Sun, 1 Feb 2026 18:21:51 -0500 Subject: [PATCH 2/4] chore: update unit test --- src/__tests__/skills-index.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/skills-index.test.tsx b/src/__tests__/skills-index.test.tsx index 769bacc..5efd7e1 100644 --- a/src/__tests__/skills-index.test.tsx +++ b/src/__tests__/skills-index.test.tsx @@ -48,10 +48,10 @@ describe('SkillsIndex', () => { it('requests the first skills page', () => { render() - // usePaginatedQuery should be called with the API endpoint and empty args + // usePaginatedQuery should be called with the API endpoint and sort/dir args expect(usePaginatedQueryMock).toHaveBeenCalledWith( expect.anything(), - {}, + { sort: 'newest', dir: 'desc' }, { initialNumItems: 25 }, ) }) From 06046c4ccb0aa33f2389f77e85d6c492d1108ecb Mon Sep 17 00:00:00 2001 From: Brian Kasper Date: Sun, 1 Feb 2026 18:37:17 -0500 Subject: [PATCH 3/4] fix: use correct indexes for skill sorting --- convex/schema.ts | 5 +++++ convex/skills.ts | 27 ++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/convex/schema.ts b/convex/schema.ts index caf9bd2..75a5d50 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -107,6 +107,11 @@ const skills = defineTable({ .index('by_stats_installs_all_time', ['statsInstallsAllTime', 'updatedAt']) .index('by_batch', ['batch']) .index('by_active_updated', ['softDeletedAt', 'updatedAt']) + .index('by_active_created', ['softDeletedAt', 'createdAt']) + .index('by_active_name', ['softDeletedAt', 'displayName']) + .index('by_active_stats_downloads', ['softDeletedAt', 'statsDownloads', 'updatedAt']) + .index('by_active_stats_stars', ['softDeletedAt', 'statsStars', 'updatedAt']) + .index('by_active_stats_installs_all_time', ['softDeletedAt', 'statsInstallsAllTime', 'updatedAt']) const souls = defineTable({ slug: v.string(), diff --git a/convex/skills.ts b/convex/skills.ts index 8a2efcf..e14d1ae 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -733,28 +733,29 @@ export const listPublicPageV2 = query({ dir: v.optional(v.union(v.literal('asc'), v.literal('desc'))), }, handler: async (ctx, args) => { - const sort = args.sort ?? 'updated' + const sort = args.sort ?? 'newest' const dir = args.dir ?? 'desc' const indexName = - sort === 'downloads' - ? 'by_stats_downloads' - : sort === 'stars' - ? 'by_stats_stars' - : sort === 'installs' - ? 'by_stats_installs_all_time' - : 'by_active_updated' - - const useActiveFilter = indexName === 'by_active_updated' + sort === 'newest' + ? 'by_active_created' + : sort === 'updated' + ? 'by_active_updated' + : sort === 'name' + ? 'by_active_name' + : sort === 'downloads' + ? 'by_active_stats_downloads' + : sort === 'stars' + ? 'by_active_stats_stars' + : 'by_active_stats_installs_all_time' const result = await paginator(ctx.db, schema) .query('skills') - .withIndex(indexName, useActiveFilter ? (q) => q.eq('softDeletedAt', undefined) : (q) => q) + .withIndex(indexName, (q) => q.eq('softDeletedAt', undefined)) .order(dir) .paginate(args.paginationOpts) - const page = useActiveFilter ? result.page : result.page.filter((s) => !s.softDeletedAt) - const items = await buildPublicSkillEntries(ctx, page) + const items = await buildPublicSkillEntries(ctx, result.page) return { ...result, page: items } }, From 219b825c6c1a96c2108f16960817b7a75e5b1bbd Mon Sep 17 00:00:00 2001 From: Brian Kasper Date: Sun, 1 Feb 2026 18:43:13 -0500 Subject: [PATCH 4/4] chore: cleanup --- convex/skills.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/convex/skills.ts b/convex/skills.ts index e14d1ae..bb3a719 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -31,6 +31,15 @@ const MAX_PUBLIC_LIST_LIMIT = 200 const MAX_LIST_BULK_LIMIT = 200 const MAX_LIST_TAKE = 1000 +const SORT_INDEXES = { + newest: 'by_active_created', + updated: 'by_active_updated', + name: 'by_active_name', + downloads: 'by_active_stats_downloads', + stars: 'by_active_stats_stars', + installs: 'by_active_stats_installs_all_time', +} as const + function isSkillVersionId( value: Id<'skillVersions'> | null | undefined, ): value is Id<'skillVersions'> { @@ -736,28 +745,21 @@ export const listPublicPageV2 = query({ const sort = args.sort ?? 'newest' const dir = args.dir ?? 'desc' - const indexName = - sort === 'newest' - ? 'by_active_created' - : sort === 'updated' - ? 'by_active_updated' - : sort === 'name' - ? 'by_active_name' - : sort === 'downloads' - ? 'by_active_stats_downloads' - : sort === 'stars' - ? 'by_active_stats_stars' - : 'by_active_stats_installs_all_time' - + // Use the index to filter out soft-deleted skills at query time. + // softDeletedAt === undefined means active (non-deleted) skills only. const result = await paginator(ctx.db, schema) .query('skills') - .withIndex(indexName, (q) => q.eq('softDeletedAt', undefined)) + .withIndex(SORT_INDEXES[sort], (q) => q.eq('softDeletedAt', undefined)) .order(dir) .paginate(args.paginationOpts) + // Build the public skill entries (fetch latestVersion + ownerHandle) const items = await buildPublicSkillEntries(ctx, result.page) - return { ...result, page: items } + return { + ...result, + page: items, + } }, })