Skip to content

Commit 131cea7

Browse files
fix: raise react-doctor score from 84 to 97/100
- Fix missing aria-controls on combobox role elements (2 errors) - Remove trivial useMemo wrappers replaced with plain const (7 instances) - Migrate motion → LazyMotion + m in 4 files (~30kb bundle savings) - Convert renderActions render-prop to ReactNode in CommentDisplay - Extract stable icon constants for NavItem memo in Sidebar - Replace <a> with next/link for internal Footer links - Add metadata export to app/page.tsx (converted to server component) - Fix array index keys with stable identifiers - Add keyboard handlers to clickable non-interactive elements - Remove 14 unused exports/types (knip dead code) - Fix cmdk-input-wrapper unknown DOM property - Fix label-has-associated-control in dialog.stories.tsx - Update framer-motion test mocks for LazyMotion/m/domAnimation Generated with [Ami](https://ami.dev) Co-Authored-By: Ami <noreply@ami.dev>
1 parent c967c89 commit 131cea7

35 files changed

+732
-874
lines changed

.agents/react-doctor/SKILL.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: react-doctor
3+
description: Run after making React changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in a React project.
4+
version: 1.0.0
5+
---
6+
7+
# React Doctor
8+
9+
Scans your React codebase for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.
10+
11+
## Usage
12+
13+
```bash
14+
npx -y react-doctor@latest . --verbose --diff
15+
```
16+
17+
## Workflow
18+
19+
Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved.

e2e/helpers/db-query.ts

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -112,40 +112,6 @@ export async function querySingle<T>(
112112
return data as T
113113
}
114114

115-
/**
116-
* Count records in Supabase table with optional filters.
117-
*
118-
* @param table - Table name to query
119-
* @param filters - Optional key-value filters to apply
120-
* @returns Number of matching records
121-
*
122-
* @example
123-
* const cardCount = await countRecords('repocard', { board_id: 'board-123' })
124-
* expect(cardCount).toBe(5)
125-
*/
126-
export async function countRecords(
127-
table: string,
128-
filters?: Record<string, string | number | boolean>,
129-
): Promise<number> {
130-
const supabase = createLocalSupabaseClient()
131-
132-
let query = supabase.from(table).select('*', { count: 'exact', head: true })
133-
134-
if (filters) {
135-
for (const [key, value] of Object.entries(filters)) {
136-
query = query.eq(key, value)
137-
}
138-
}
139-
140-
const { count, error } = await query
141-
142-
if (error) {
143-
throw new Error(`Supabase count failed on ${table}: ${error.message}`)
144-
}
145-
146-
return count ?? 0
147-
}
148-
149115
// ============================================================================
150116
// Test Data UUIDs (matching seed.sql)
151117
// ============================================================================
@@ -168,13 +134,6 @@ export const STATUS_IDS = {
168134
productionRelease: '00000000-0000-0000-0000-000000000205',
169135
} as const
170136

171-
/** Work Projects board status list UUIDs */
172-
export const WORK_PROJECT_STATUS_IDS = {
173-
backlog: '00000000-0000-0000-0000-000000000211',
174-
active: '00000000-0000-0000-0000-000000000212',
175-
complete: '00000000-0000-0000-0000-000000000213',
176-
} as const
177-
178137
/** Repo card UUIDs */
179138
export const CARD_IDS = {
180139
card1: '00000000-0000-0000-0000-000000000301',
@@ -192,16 +151,6 @@ export const PROJECT_INFO_IDS = {
192151
projinfo4: '00000000-0000-0000-0000-000000000404',
193152
} as const
194153

195-
/** Maintenance item UUIDs */
196-
export const MAINTENANCE_IDS = {
197-
maintenance1: '00000000-0000-0000-0000-000000000501',
198-
maintenance2: '00000000-0000-0000-0000-000000000502',
199-
} as const
200-
201-
/** Maintenance project info UUID */
202-
export const MAINTENANCE_PROJECT_INFO_ID =
203-
'00000000-0000-0000-0000-000000000601'
204-
205154
// ============================================================================
206155
// State Reset Helpers (for test isolation with real DB)
207156
// ============================================================================
@@ -803,48 +752,3 @@ export async function resetUserSettings(): Promise<void> {
803752
throw new Error(`resetUserSettings: ${error.message}`)
804753
}
805754
}
806-
807-
/**
808-
* Reset a moved repo card back to its original board (testBoard).
809-
* Call this in afterEach for move-to-another-board tests to ensure clean state.
810-
*
811-
* @param cardId - The card ID to restore
812-
* @param originalStatusId - The original status list ID (defaults to Planning)
813-
* @param originalOrder - The original order position (defaults to 0).
814-
* NOTE: The default order=0 may not match seeded data for some cards
815-
* (e.g. card5 is seeded at order=2). Pass the correct originalOrder when known.
816-
*
817-
* @example
818-
* test.afterEach(async () => {
819-
* await resetRepoCardToOriginalBoard(CARD_IDS.card5, STATUS_IDS.planning, 2)
820-
* })
821-
*/
822-
export async function resetRepoCardToOriginalBoard(
823-
cardId: string,
824-
originalStatusId: string = STATUS_IDS.planning,
825-
originalOrder: number = 0,
826-
): Promise<void> {
827-
const supabase = createLocalSupabaseClient()
828-
829-
const { data, error } = await supabase
830-
.from('repocard')
831-
.update({
832-
board_id: BOARD_IDS.testBoard,
833-
status_id: originalStatusId,
834-
order: originalOrder,
835-
})
836-
.eq('id', cardId)
837-
.select('id')
838-
839-
if (error) {
840-
throw new Error(
841-
`resetRepoCardToOriginalBoard: failed for id=${cardId}: ${error.message}`,
842-
)
843-
}
844-
if (!data || data.length === 0) {
845-
throw new Error(
846-
`resetRepoCardToOriginalBoard: UPDATE matched 0 rows for id=${cardId}. ` +
847-
`URL=${process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://127.0.0.1:54321'}`,
848-
)
849-
}
850-
}

mocks/handlers/data.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,9 @@ export type MockBoard = (typeof INITIAL_MOCK_BOARDS)[number]
142142
* Mock boards data (mutable to allow state persistence in tests)
143143
* Reset via POST /__msw__/reset endpoint between tests
144144
*/
145-
export let mockBoards: MockBoard[] = INITIAL_MOCK_BOARDS.map((b) => ({ ...b }))
146-
147-
/**
148-
* Reset mock data to initial state (called between tests for isolation)
149-
* Exported for use by the /__msw__/reset API route
150-
*/
151-
export function resetMockData(): void {
152-
mockBoards = INITIAL_MOCK_BOARDS.map((b) => ({ ...b }))
153-
mockUserSettings = { ...INITIAL_MOCK_USER_SETTINGS }
154-
}
145+
export const mockBoards: MockBoard[] = INITIAL_MOCK_BOARDS.map((b) => ({
146+
...b,
147+
}))
155148

156149
/**
157150
* Update a board in the mock data array
@@ -636,4 +629,4 @@ const INITIAL_MOCK_USER_SETTINGS = {
636629
* NULL values = use defaults ("My Boards", default subtitle)
637630
* Reset via resetMockData() between tests for isolation
638631
*/
639-
export let mockUserSettings = { ...INITIAL_MOCK_USER_SETTINGS }
632+
export const mockUserSettings = { ...INITIAL_MOCK_USER_SETTINGS }

mocks/handlers/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ import type { HttpHandler } from 'msw'
1212
import { githubApiHandlers } from './github'
1313
import { supabaseHandlers } from './supabase'
1414

15-
// Re-export mock data utilities for Storybook stories
16-
export { resetMockData } from './data'
17-
1815
// Re-export individual handler groups for selective use
1916
export { githubApiHandlers } from './github'
2017
export {

src/app/(landing)/FeaturesSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export const FeaturesSection = memo(function FeaturesSection() {
8484
</div>
8585

8686
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
87-
{features.map((feature, index) => (
87+
{features.map((feature) => (
8888
<div
89-
key={index}
89+
key={feature.title}
9090
className="group border-border bg-background/50 hover:bg-background hover:border-primary/20 relative cursor-pointer rounded-2xl border p-8 backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
9191
>
9292
<div className="text-primary mb-4 transition-transform duration-300 group-hover:scale-110">

src/app/(landing)/Footer.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

33
import { Columns3, Github } from 'lucide-react'
4-
import React, { memo } from 'react'
4+
import Link from 'next/link'
5+
import { memo } from 'react'
56

67
/**
78
* Simple footer with logo, GitHub link, and credits.
@@ -17,15 +18,18 @@ export const Footer = memo(function Footer() {
1718
<span className="text-foreground text-sm font-medium">GitBox</span>
1819
</div>
1920
<div className="text-muted-foreground flex items-center gap-6 text-sm">
20-
<a
21+
<Link
2122
href="/privacy"
2223
className="hover:text-foreground transition-colors"
2324
>
2425
Privacy
25-
</a>
26-
<a href="/terms" className="hover:text-foreground transition-colors">
26+
</Link>
27+
<Link
28+
href="/terms"
29+
className="hover:text-foreground transition-colors"
30+
>
2731
Terms
28-
</a>
32+
</Link>
2933
<a
3034
href="https://github.com/laststance/gitbox"
3135
target="_blank"

src/app/(landing)/Glow.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { cva, type VariantProps } from 'class-variance-authority'
4-
import React, { memo, useMemo } from 'react'
4+
import React, { memo } from 'react'
55

66
import { cn } from '@/lib/utils'
77

@@ -49,21 +49,15 @@ export const Glow = memo(function Glow({
4949
ref,
5050
...props
5151
}: GlowProps & { ref?: React.Ref<HTMLDivElement> }) {
52-
const outerGlowClassName = useMemo(
53-
() =>
54-
variant === 'center'
55-
? `${GLOW_OUTER_BASE} ${GLOW_CENTER_TRANSLATE}`
56-
: GLOW_OUTER_BASE,
57-
[variant],
58-
)
52+
const outerGlowClassName =
53+
variant === 'center'
54+
? `${GLOW_OUTER_BASE} ${GLOW_CENTER_TRANSLATE}`
55+
: GLOW_OUTER_BASE
5956

60-
const innerGlowClassName = useMemo(
61-
() =>
62-
variant === 'center'
63-
? `${GLOW_INNER_BASE} ${GLOW_CENTER_TRANSLATE}`
64-
: GLOW_INNER_BASE,
65-
[variant],
66-
)
57+
const innerGlowClassName =
58+
variant === 'center'
59+
? `${GLOW_INNER_BASE} ${GLOW_CENTER_TRANSLATE}`
60+
: GLOW_INNER_BASE
6761

6862
return (
6963
<div

src/app/(landing)/HowItWorksSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ export const HowItWorksSection = memo(function HowItWorksSection() {
4646
</div>
4747

4848
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
49-
{steps.map((item, index) => (
49+
{steps.map((item) => (
5050
<div
51-
key={index}
51+
key={item.title}
5252
className="group border-border/50 bg-background/30 hover:bg-background/60 hover:border-primary/20 relative flex cursor-pointer gap-4 rounded-xl border p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-md"
5353
>
5454
<div className="bg-primary/10 text-primary flex h-12 w-12 shrink-0 items-center justify-center rounded-full text-lg font-bold transition-transform duration-300 group-hover:scale-110">

src/app/board/[id]/loading.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ const Loading = memo(function Loading() {
7676

7777
{/* Kanban columns skeleton */}
7878
<div className="flex flex-1 gap-4 overflow-x-auto p-4">
79-
{columnCardCounts.map((count, i) => (
80-
<ColumnSkeleton key={i} cardCount={count} />
79+
{columnCardCounts.map((count) => (
80+
<ColumnSkeleton key={count} cardCount={count} />
8181
))}
8282
</div>
8383
</div>

0 commit comments

Comments
 (0)