diff --git a/convex/crons.ts b/convex/crons.ts index c35b2ce..8c6e7a2 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -31,4 +31,11 @@ crons.interval( {}, ) +crons.interval( + 'global-stats-update', + { minutes: 60 }, + internal.statsMaintenance.updateGlobalStatsInternal, + {}, +) + export default crons diff --git a/convex/schema.ts b/convex/schema.ts index caf9bd2..b861e6c 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -270,6 +270,12 @@ const skillStatBackfillState = defineTable({ updatedAt: v.number(), }).index('by_key', ['key']) +const globalStats = defineTable({ + key: v.string(), + activeSkillsCount: v.number(), + updatedAt: v.number(), +}).index('by_key', ['key']) + const skillStatEvents = defineTable({ skillId: v.id('skills'), kind: v.union( @@ -459,6 +465,7 @@ export default defineSchema({ skillDailyStats, skillLeaderboards, skillStatBackfillState, + globalStats, skillStatEvents, skillStatUpdateCursors, comments, diff --git a/convex/skills.ts b/convex/skills.ts index be38017..ecf2877 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -162,7 +162,8 @@ async function hardDeleteSkill( if (related._id === skill._id) continue if (related.canonicalSkillId === skill._id || related.forkOf?.skillId === skill._id) { await ctx.db.patch(related._id, { - canonicalSkillId: related.canonicalSkillId === skill._id ? undefined : related.canonicalSkillId, + canonicalSkillId: + related.canonicalSkillId === skill._id ? undefined : related.canonicalSkillId, forkOf: related.forkOf?.skillId === skill._id ? undefined : related.forkOf, updatedAt: now, }) @@ -740,6 +741,17 @@ export const listPublicPageV2 = query({ }, }) +export const countPublicSkills = query({ + args: {}, + handler: async (ctx) => { + const stats = await ctx.db + .query('globalStats') + .withIndex('by_key', (q) => q.eq('key', 'default')) + .unique() + return stats?.activeSkillsCount ?? 0 + }, +}) + function sortToIndex( sort: 'downloads' | 'stars' | 'installsCurrent' | 'installsAllTime', ): diff --git a/convex/statsMaintenance.ts b/convex/statsMaintenance.ts index 35f8206..3e42e95 100644 --- a/convex/statsMaintenance.ts +++ b/convex/statsMaintenance.ts @@ -203,3 +203,30 @@ function buildSkillStatPatch(skill: Doc<'skills'>) { function clampInt(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max) } + +export const updateGlobalStatsInternal = internalMutation({ + args: {}, + handler: async (ctx) => { + const skills = await ctx.db + .query('skills') + .withIndex('by_active_updated', (q) => q.eq('softDeletedAt', undefined)) + .collect() + + const count = skills.length + const now = Date.now() + const existing = await ctx.db + .query('globalStats') + .withIndex('by_key', (q) => q.eq('key', 'default')) + .unique() + + if (existing) { + await ctx.db.patch(existing._id, { activeSkillsCount: count, updatedAt: now }) + } else { + await ctx.db.insert('globalStats', { + key: 'default', + activeSkillsCount: count, + updatedAt: now, + }) + } + }, +}) diff --git a/src/lib/og.ts b/src/lib/og.ts index a66631d..31dd79f 100644 --- a/src/lib/og.ts +++ b/src/lib/og.ts @@ -1,4 +1,4 @@ -import { getOnlyCrabsSiteUrl, getClawHubSiteUrl } from './site' +import { getClawHubSiteUrl, getOnlyCrabsSiteUrl } from './site' type SkillMetaSource = { slug: string diff --git a/src/routes/skills/index.tsx b/src/routes/skills/index.tsx index bf0a2a6..00b641d 100644 --- a/src/routes/skills/index.tsx +++ b/src/routes/skills/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link } from '@tanstack/react-router' -import { useAction } from 'convex/react' +import { useAction, useQuery } from 'convex/react' import { usePaginatedQuery } from 'convex-helpers/react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { api } from '../../../convex/_generated/api' @@ -73,6 +73,7 @@ export function SkillsIndex() { const [isSearching, setIsSearching] = useState(false) const searchRequest = useRef(0) const loadMoreRef = useRef(null) + const totalSkills = useQuery(api.skills.countPublicSkills) const searchInputRef = useRef(null) const trimmedQuery = useMemo(() => query.trim(), [query]) @@ -225,7 +226,8 @@ export function SkillsIndex() {

- Skills + Skills{' '} + {totalSkills !== undefined && {totalSkills}}

{isLoadingSkills