Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions convex/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ crons.interval(
{},
)

crons.interval(
'global-stats-update',
{ minutes: 60 },
internal.statsMaintenance.updateGlobalStatsInternal,
{},
)

export default crons
7 changes: 7 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -459,6 +465,7 @@ export default defineSchema({
skillDailyStats,
skillLeaderboards,
skillStatBackfillState,
globalStats,
skillStatEvents,
skillStatUpdateCursors,
comments,
Expand Down
14 changes: 13 additions & 1 deletion convex/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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',
):
Expand Down
27 changes: 27 additions & 0 deletions convex/statsMaintenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
},
})
2 changes: 1 addition & 1 deletion src/lib/og.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getOnlyCrabsSiteUrl, getClawHubSiteUrl } from './site'
import { getClawHubSiteUrl, getOnlyCrabsSiteUrl } from './site'

type SkillMetaSource = {
slug: string
Expand Down
6 changes: 4 additions & 2 deletions src/routes/skills/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -73,6 +73,7 @@ export function SkillsIndex() {
const [isSearching, setIsSearching] = useState(false)
const searchRequest = useRef(0)
const loadMoreRef = useRef<HTMLDivElement | null>(null)
const totalSkills = useQuery(api.skills.countPublicSkills)

const searchInputRef = useRef<HTMLInputElement>(null)
const trimmedQuery = useMemo(() => query.trim(), [query])
Expand Down Expand Up @@ -225,7 +226,8 @@ export function SkillsIndex() {
<header className="skills-header">
<div>
<h1 className="section-title" style={{ marginBottom: 8 }}>
Skills
Skills{' '}
{totalSkills !== undefined && <span style={{ opacity: 0.5 }}>{totalSkills}</span>}
</h1>
Comment on lines 228 to 231
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] The header currently renders Skills{' '}{totalSkills...} which shows a bare number with no delimiter/label. This can read as part of the title and may be confusing for screen readers. Consider formatting as e.g. Skills (123) or adding an aria-label/visually-hidden label for the count.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/skills/index.tsx
Line: 228:231

Comment:
[P3] The header currently renders `Skills{' '}{totalSkills...}` which shows a bare number with no delimiter/label. This can read as part of the title and may be confusing for screen readers. Consider formatting as e.g. `Skills (123)` or adding an `aria-label`/visually-hidden label for the count.

How can I resolve this? If you propose a fix, please make it concise.

<p className="section-subtitle" style={{ marginBottom: 0 }}>
{isLoadingSkills
Expand Down