Skip to content

Commit 425e096

Browse files
committed
feat: remove "Tested with" LLM logic and add virtualized infinite scroll to /souls
Remove the testedWithModels field and all associated UI (model badges, model filter, JSON-LD targetProduct) from schema, backend, types, pages, components, and documentation. Replace the fixed 50-item load on /souls with cursor-based pagination (24 per page) and TanStack Virtual row virtualization so only visible cards are in the DOM. Infinite scroll loads the next page automatically as the user scrolls near the bottom. Made-with: Cursor
1 parent 186afbf commit 425e096

27 files changed

Lines changed: 268 additions & 270 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ npm-debug.log*
3232
.vercel
3333
.env*.local
3434

35+
./agents
3536
**/.claude
3637
.claude
3738
.cursor

AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# AGENTS.md
2+
3+
## Learned User Preferences
4+
5+
- Prefer TanStack ecosystem libraries when applicable (e.g. TanStack Virtual for list virtualization)
6+
- Test UI changes in the browser after implementation; do not assume visual correctness from code alone
7+
- Based in Canada; privacy compliance must account for PIPEDA and Quebec Law 25
8+
- No session replay or invasive tracking without a consent banner; basic analytics is acceptable under implied consent
9+
- Uses Typefully for drafting social media announcements
10+
- Prefers `pnpm validate` as the single pre-commit check (Biome lint/format + TypeScript typecheck)
11+
12+
## Learned Workspace Facts
13+
14+
- Convex dev deployment slug: `careful-kangaroo-28` (site URL: `https://careful-kangaroo-28.convex.site`)
15+
- Dev server uses `portless` tool; local URL is `http://souls.localhost:<port>/` (not `localhost:3000`)
16+
- Convex env var `SITE_URL` must match the local portless URL for auth redirects to work
17+
- OpenPanel analytics is proxied through `/api/op`; only active in production (gated by `AnalyticsProvider`)
18+
- `@tanstack/react-virtual` is used for the `/souls` browse page grid virtualization with cursor-based infinite scroll
19+
- GitHub OAuth env vars (`AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`) live in Convex dashboard, not `.env.local`

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ You can also **fork** an existing soul (from its page) or **edit** your own soul
4545
- [ ] Category selected (see list below)
4646
- [ ] 3–5 relevant tags selected
4747
- [ ] Core truths, boundaries, and vibe are in the markdown
48-
- [ ] You've tested the soul with at least one LLM
48+
- [ ] You've tested the soul before publishing
4949
- [ ] No typos or grammar issues
5050
- [ ] Slug is lowercase-with-hyphens and unique
5151

apps/web/__mocks__/convex-react.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export function useMutation(_mutation: any) {
1313
}
1414

1515
export function useConvex() {
16-
return {}
16+
return {
17+
query: async () => ({ items: [], nextCursor: null }),
18+
}
1719
}
1820

1921
export function useConvexAuth() {

apps/web/app/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export default async function Home() {
6868
upvotes: item.soul.stats?.upvotes || 0,
6969
versions: item.soul.stats?.versions ?? 1,
7070
featured: item.soul.featured || false,
71-
tested_with: (item.soul.testedWithModels || []).map((t: { model: string }) => t.model),
7271
category_id: item.category?._id || '',
7372
category: item.category
7473
? {

apps/web/app/souls/[handle]/[slug]/page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ interface RelatedSoulData {
4343
upvotes?: number
4444
}
4545
featured?: boolean
46-
testedWithModels?: Array<{ model: string }>
4746
createdAt: number
4847
updatedAt: number
4948
}
@@ -129,7 +128,6 @@ export default async function SoulPage({ params }: PageProps) {
129128
comments: soul.stats?.comments || 0,
130129
views: soul.stats?.views || 0,
131130
},
132-
testedWithModels: (soul.testedWithModels || []).map((t: { model: string }) => t.model),
133131
featured: soul.featured || false,
134132
createdAt: soul.createdAt,
135133
updatedAt: soul.updatedAt,
@@ -174,7 +172,6 @@ export default async function SoulPage({ params }: PageProps) {
174172
stars: s.stats?.stars || 0,
175173
upvotes: s.stats?.upvotes || 0,
176174
featured: s.featured || false,
177-
tested_with: (s.testedWithModels || []).map((t) => t.model),
178175
category_id: category?._id || '',
179176
category: category
180177
? {
@@ -202,7 +199,6 @@ export default async function SoulPage({ params }: PageProps) {
202199
stars: s.stats?.stars || 0,
203200
upvotes: s.stats?.upvotes || 0,
204201
featured: s.featured || false,
205-
tested_with: [],
206202
category_id: category?._id || '',
207203
category: category
208204
? {
@@ -238,7 +234,6 @@ export default async function SoulPage({ params }: PageProps) {
238234
stars={transformedData.soul.stats.stars}
239235
createdAt={soul.createdAt}
240236
updatedAt={soul.updatedAt}
241-
testedWithModels={soul.testedWithModels}
242237
/>
243238
<BreadcrumbSchema
244239
items={[

apps/web/app/souls/page.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,28 @@ interface PageProps {
4040
searchParams: Promise<{
4141
q?: string
4242
category?: string
43-
model?: string
4443
sort?: 'recent' | 'published' | 'popular' | 'trending' | 'hot'
4544
featured?: string
4645
}>
4746
}
4847

4948
export default async function SoulsPage({ searchParams }: PageProps) {
50-
// Await searchParams (Next.js 15+ async searchParams)
5149
const params = await searchParams
5250

5351
const categorySlug = params.category
54-
const model = params.model
5552
const sort = params.sort ?? 'recent'
5653
const featured = params.featured === 'true'
5754

58-
// Fetch data on the server in parallel
5955
const [categoriesData, soulsData] = await Promise.all([
6056
getCategoriesList(),
6157
getSoulsList({
6258
categorySlug,
63-
model,
6459
sort,
6560
featured: featured || undefined,
66-
limit: 50,
61+
limit: 24,
6762
}),
6863
])
6964

70-
// Transform categories
7165
const categories: Category[] = (categoriesData || []).map((cat: CategoryData) => ({
7266
id: cat._id,
7367
slug: cat.slug,
@@ -77,7 +71,6 @@ export default async function SoulsPage({ searchParams }: PageProps) {
7771
color: cat.color || '#878787',
7872
}))
7973

80-
// Transform souls
8174
const souls: Soul[] = (soulsData?.items || []).map((item: SoulListItem) => ({
8275
id: item.soul._id,
8376
slug: item.soul.slug,
@@ -90,7 +83,6 @@ export default async function SoulsPage({ searchParams }: PageProps) {
9083
stars: item.soul.stats?.stars || 0,
9184
upvotes: item.soul.stats?.upvotes || 0,
9285
featured: item.soul.featured || false,
93-
tested_with: (item.soul.testedWithModels || []).map((t: { model: string }) => t.model),
9486
category_id: item.category?._id || '',
9587
category: item.category
9688
? {
@@ -107,6 +99,8 @@ export default async function SoulsPage({ searchParams }: PageProps) {
10799
updated_at: new Date(item.soul.updatedAt).toISOString(),
108100
}))
109101

102+
const initialCursor = soulsData?.nextCursor ?? null
103+
110104
return (
111105
<>
112106
<CollectionPageSchema
@@ -116,7 +110,11 @@ export default async function SoulsPage({ searchParams }: PageProps) {
116110
itemCount={souls.length}
117111
/>
118112
<BreadcrumbSchema items={[{ name: 'Souls', url: '/souls' }]} />
119-
<BrowseContent initialCategories={categories} initialSouls={souls} />
113+
<BrowseContent
114+
initialCategories={categories}
115+
initialSouls={souls}
116+
initialCursor={initialCursor}
117+
/>
120118
</>
121119
)
122120
}

apps/web/components/browse/__tests__/browse-content.test.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ vi.mock('nuqs', () => ({
1616
{
1717
q: '',
1818
category: null,
19-
model: null,
2019
sort: 'recent',
2120
featured: false,
2221
},
@@ -59,6 +58,15 @@ vi.mock('@/components/souls/soul-card-grid', () => ({
5958
React.createElement('div', {}, children),
6059
}))
6160

61+
vi.mock('@tanstack/react-virtual', () => ({
62+
useWindowVirtualizer: () => ({
63+
getVirtualItems: () => [{ key: '0', index: 0, start: 0, size: 220, end: 220 }],
64+
getTotalSize: () => 220,
65+
measureElement: () => {},
66+
options: { scrollMargin: 0 },
67+
}),
68+
}))
69+
6270
vi.mock('next/link', () => ({
6371
default: ({
6472
children,
@@ -100,17 +108,27 @@ const mockSoul: Soul = {
100108

101109
describe('BrowseContent', () => {
102110
it('renders Browse heading', () => {
103-
render(<BrowseContent initialCategories={[mockCategory]} initialSouls={[]} />)
111+
render(
112+
<BrowseContent initialCategories={[mockCategory]} initialSouls={[]} initialCursor={null} />
113+
)
104114
expect(screen.getByRole('heading', { name: 'Browse Souls', level: 1 })).toBeInTheDocument()
105115
})
106116

107117
it('renders empty state when no souls match', () => {
108-
render(<BrowseContent initialCategories={[mockCategory]} initialSouls={[]} />)
118+
render(
119+
<BrowseContent initialCategories={[mockCategory]} initialSouls={[]} initialCursor={null} />
120+
)
109121
expect(screen.getByText('No souls found')).toBeInTheDocument()
110122
})
111123

112124
it('renders soul cards when souls provided', () => {
113-
render(<BrowseContent initialCategories={[mockCategory]} initialSouls={[mockSoul]} />)
125+
render(
126+
<BrowseContent
127+
initialCategories={[mockCategory]}
128+
initialSouls={[mockSoul]}
129+
initialCursor={null}
130+
/>
131+
)
114132
expect(screen.getByText('Test Soul')).toBeInTheDocument()
115133
})
116134
})

0 commit comments

Comments
 (0)