oh-my-ag 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/skills/_shared/api-contracts/README.md +56 -0
- package/.agent/skills/_shared/api-contracts/template.md +88 -0
- package/.agent/skills/_shared/clarification-protocol.md +217 -0
- package/.agent/skills/_shared/common-checklist.md +31 -0
- package/.agent/skills/_shared/context-budget.md +118 -0
- package/.agent/skills/_shared/context-loading.md +105 -0
- package/.agent/skills/_shared/difficulty-guide.md +55 -0
- package/.agent/skills/_shared/lessons-learned.md +113 -0
- package/.agent/skills/_shared/memory-protocol.md +79 -0
- package/.agent/skills/_shared/reasoning-templates.md +161 -0
- package/.agent/skills/_shared/skill-routing.md +80 -0
- package/.agent/skills/_shared/verify.sh +252 -0
- package/.agent/skills/backend-agent/SKILL.md +47 -0
- package/.agent/skills/backend-agent/resources/api-template.py +326 -0
- package/.agent/skills/backend-agent/resources/checklist.md +36 -0
- package/.agent/skills/backend-agent/resources/error-playbook.md +98 -0
- package/.agent/skills/backend-agent/resources/examples.md +85 -0
- package/.agent/skills/backend-agent/resources/execution-protocol.md +45 -0
- package/.agent/skills/backend-agent/resources/snippets.md +197 -0
- package/.agent/skills/backend-agent/resources/tech-stack.md +39 -0
- package/.agent/skills/commit/SKILL.md +121 -0
- package/.agent/skills/commit/config/commit-config.yaml +55 -0
- package/.agent/skills/commit/resources/conventional-commits.md +166 -0
- package/.agent/skills/debug-agent/SKILL.md +51 -0
- package/.agent/skills/debug-agent/resources/bug-report-template.md +332 -0
- package/.agent/skills/debug-agent/resources/checklist.md +30 -0
- package/.agent/skills/debug-agent/resources/common-patterns.md +734 -0
- package/.agent/skills/debug-agent/resources/debugging-checklist.md +362 -0
- package/.agent/skills/debug-agent/resources/error-playbook.md +94 -0
- package/.agent/skills/debug-agent/resources/examples.md +87 -0
- package/.agent/skills/debug-agent/resources/execution-protocol.md +51 -0
- package/.agent/skills/frontend-agent/SKILL.md +48 -0
- package/.agent/skills/frontend-agent/resources/checklist.md +38 -0
- package/.agent/skills/frontend-agent/resources/component-template.tsx +92 -0
- package/.agent/skills/frontend-agent/resources/error-playbook.md +108 -0
- package/.agent/skills/frontend-agent/resources/examples.md +77 -0
- package/.agent/skills/frontend-agent/resources/execution-protocol.md +49 -0
- package/.agent/skills/frontend-agent/resources/snippets.md +205 -0
- package/.agent/skills/frontend-agent/resources/tailwind-rules.md +343 -0
- package/.agent/skills/frontend-agent/resources/tech-stack.md +36 -0
- package/.agent/skills/mobile-agent/SKILL.md +46 -0
- package/.agent/skills/mobile-agent/resources/checklist.md +35 -0
- package/.agent/skills/mobile-agent/resources/error-playbook.md +106 -0
- package/.agent/skills/mobile-agent/resources/examples.md +79 -0
- package/.agent/skills/mobile-agent/resources/execution-protocol.md +49 -0
- package/.agent/skills/mobile-agent/resources/screen-template.dart +298 -0
- package/.agent/skills/mobile-agent/resources/snippets.md +235 -0
- package/.agent/skills/mobile-agent/resources/tech-stack.md +45 -0
- package/.agent/skills/orchestrator/SKILL.md +99 -0
- package/.agent/skills/orchestrator/config/cli-config.yaml +78 -0
- package/.agent/skills/orchestrator/resources/memory-schema.md +212 -0
- package/.agent/skills/orchestrator/resources/subagent-prompt-template.md +153 -0
- package/.agent/skills/orchestrator/scripts/parallel-run.sh +330 -0
- package/.agent/skills/orchestrator/scripts/spawn-agent.sh +263 -0
- package/.agent/skills/orchestrator/templates/backend-task.md +18 -0
- package/.agent/skills/orchestrator/templates/debug-task.md +16 -0
- package/.agent/skills/orchestrator/templates/frontend-task.md +17 -0
- package/.agent/skills/orchestrator/templates/mobile-task.md +17 -0
- package/.agent/skills/orchestrator/templates/qa-task.md +16 -0
- package/.agent/skills/orchestrator/templates/tasks-example.yaml +15 -0
- package/.agent/skills/pm-agent/SKILL.md +47 -0
- package/.agent/skills/pm-agent/resources/error-playbook.md +75 -0
- package/.agent/skills/pm-agent/resources/examples.md +121 -0
- package/.agent/skills/pm-agent/resources/execution-protocol.md +46 -0
- package/.agent/skills/pm-agent/resources/task-template.json +57 -0
- package/.agent/skills/qa-agent/SKILL.md +43 -0
- package/.agent/skills/qa-agent/resources/checklist.md +294 -0
- package/.agent/skills/qa-agent/resources/error-playbook.md +95 -0
- package/.agent/skills/qa-agent/resources/examples.md +100 -0
- package/.agent/skills/qa-agent/resources/execution-protocol.md +50 -0
- package/.agent/skills/qa-agent/resources/self-check.md +27 -0
- package/.agent/skills/workflow-guide/SKILL.md +57 -0
- package/.agent/skills/workflow-guide/resources/examples.md +68 -0
- package/README.ko.md +459 -0
- package/README.md +563 -0
- package/bin/cli.js +205 -0
- package/package.json +75 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Template for Frontend Agent
|
|
3
|
+
*
|
|
4
|
+
* This is a reference template for creating new components.
|
|
5
|
+
* Follow this structure for consistency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
// 1. Type Definitions
|
|
11
|
+
interface ComponentNameProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
// Add specific props here
|
|
15
|
+
variant?: 'default' | 'primary' | 'secondary';
|
|
16
|
+
size?: 'sm' | 'md' | 'lg';
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Main Component
|
|
22
|
+
export function ComponentName({
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
variant = 'default',
|
|
26
|
+
size = 'md',
|
|
27
|
+
onClick,
|
|
28
|
+
disabled = false,
|
|
29
|
+
}: ComponentNameProps) {
|
|
30
|
+
// 3. Hooks (if needed)
|
|
31
|
+
// const [state, setState] = useState();
|
|
32
|
+
|
|
33
|
+
// 4. Effects (if needed)
|
|
34
|
+
// useEffect(() => {}, []);
|
|
35
|
+
|
|
36
|
+
// 5. Event Handlers
|
|
37
|
+
const handleClick = () => {
|
|
38
|
+
if (disabled) return;
|
|
39
|
+
onClick?.();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// 6. Computed Values
|
|
43
|
+
const classes = cn(
|
|
44
|
+
// Base styles
|
|
45
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
|
46
|
+
// Variants
|
|
47
|
+
{
|
|
48
|
+
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
|
|
49
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
|
50
|
+
'bg-background text-foreground hover:bg-accent': variant === 'default',
|
|
51
|
+
},
|
|
52
|
+
// Sizes
|
|
53
|
+
{
|
|
54
|
+
'h-8 px-3 text-sm': size === 'sm',
|
|
55
|
+
'h-10 px-4 text-base': size === 'md',
|
|
56
|
+
'h-12 px-6 text-lg': size === 'lg',
|
|
57
|
+
},
|
|
58
|
+
// States
|
|
59
|
+
{
|
|
60
|
+
'opacity-50 cursor-not-allowed': disabled,
|
|
61
|
+
},
|
|
62
|
+
className
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 7. Render
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className={classes}
|
|
70
|
+
onClick={handleClick}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
aria-disabled={disabled}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 8. Sub-components (if needed)
|
|
80
|
+
ComponentName.Slot = function ComponentNameSlot({
|
|
81
|
+
className,
|
|
82
|
+
children,
|
|
83
|
+
}: ComponentNameProps) {
|
|
84
|
+
return (
|
|
85
|
+
<div className={cn('flex items-center gap-2', className)}>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// 9. Display Name (for debugging)
|
|
92
|
+
ComponentName.displayName = 'ComponentName';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Frontend Agent - Error Recovery Playbook
|
|
2
|
+
|
|
3
|
+
When you encounter a failure, find the matching scenario and follow the recovery steps.
|
|
4
|
+
Do NOT stop or ask for help until you have exhausted the playbook.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## TypeScript Compilation Error
|
|
9
|
+
|
|
10
|
+
**Symptoms**: `TS2322`, `TS2345`, `Type X is not assignable to type Y`
|
|
11
|
+
|
|
12
|
+
1. Read the error — which file, which line, which types conflict
|
|
13
|
+
2. Check: is the interface/type definition correct?
|
|
14
|
+
3. Check: is the API response type matching the expected shape?
|
|
15
|
+
4. If API mismatch: update the type to match actual response (don't cast with `as any`)
|
|
16
|
+
5. If generic issue: use explicit type parameter `<Type>` instead of inference
|
|
17
|
+
6. **절대 하지 말 것**: `@ts-ignore`, `as any` — 타입 문제를 해결하지 않고 숨기는 것
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Build Error
|
|
22
|
+
|
|
23
|
+
**Symptoms**: `next build` fails, `Module not found`, `SyntaxError`
|
|
24
|
+
|
|
25
|
+
1. Read the full error — which module, which file
|
|
26
|
+
2. If missing dependency: note in result as "requires `npm install X`" — do NOT install yourself
|
|
27
|
+
3. If import path wrong: use `search_for_pattern("export.*ComponentName")` to find actual path
|
|
28
|
+
4. If dynamic import issue: ensure component is client-side (`'use client'`)
|
|
29
|
+
5. Re-run build after fix to confirm
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Test Failure
|
|
34
|
+
|
|
35
|
+
**Symptoms**: `vitest` FAILED, `expect(X).toBe(Y)` assertion errors
|
|
36
|
+
|
|
37
|
+
1. Read the error — expected vs received, which test file
|
|
38
|
+
2. `find_symbol("ComponentName")` to check current implementation
|
|
39
|
+
3. Determine: test outdated or implementation wrong?
|
|
40
|
+
- Test expects old behavior → update test
|
|
41
|
+
- Component bug → fix component
|
|
42
|
+
4. Re-run the specific test: `npx vitest run path/to/test.ts`
|
|
43
|
+
5. **3회 실패 시**: 다른 접근 방식 시도. progress에 기록
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Hydration Mismatch (Next.js)
|
|
48
|
+
|
|
49
|
+
**Symptoms**: `Hydration failed`, `Text content does not match server-rendered HTML`
|
|
50
|
+
|
|
51
|
+
1. Find the component that renders differently on server vs client
|
|
52
|
+
2. Common causes:
|
|
53
|
+
- `Date.now()` or `Math.random()` in render
|
|
54
|
+
- Browser-only APIs (`window`, `localStorage`) without `useEffect`
|
|
55
|
+
- Conditional rendering based on client-only state
|
|
56
|
+
3. Fix: wrap client-only code in `useEffect` + state, or use `'use client'`
|
|
57
|
+
4. If third-party component: wrap with `dynamic(() => import(...), { ssr: false })`
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## API Integration Error
|
|
62
|
+
|
|
63
|
+
**Symptoms**: `Network Error`, `CORS`, `401 Unauthorized`, wrong data shape
|
|
64
|
+
|
|
65
|
+
1. **CORS**: Check backend CORS config — is frontend origin allowed?
|
|
66
|
+
2. **401**: Check token — is it in the header? is it expired?
|
|
67
|
+
3. **Wrong data**: Log `response.data` and compare with expected type
|
|
68
|
+
4. **Network Error**: Is the backend running? Correct port?
|
|
69
|
+
5. If backend isn't your responsibility: document the expected API contract in result
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Styling / Layout Broken
|
|
74
|
+
|
|
75
|
+
**Symptoms**: Component renders but looks wrong, responsive breakpoint fails
|
|
76
|
+
|
|
77
|
+
1. Check Tailwind classes — typo? wrong breakpoint prefix?
|
|
78
|
+
2. Check parent container — is it blocking layout? (`overflow-hidden`, fixed width)
|
|
79
|
+
3. Test at specific breakpoints: 320px, 768px, 1024px, 1440px
|
|
80
|
+
4. Use browser DevTools to inspect computed styles
|
|
81
|
+
5. If dark mode issue: check `dark:` variants applied
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Rate Limit / Quota Error (Gemini API)
|
|
86
|
+
|
|
87
|
+
**Symptoms**: `429`, `RESOURCE_EXHAUSTED`, `rate limit exceeded`
|
|
88
|
+
|
|
89
|
+
1. **즉시 멈춤** — 추가 API 호출 하지 말 것
|
|
90
|
+
2. 현재까지 작업을 `progress-{agent-id}.md`에 저장
|
|
91
|
+
3. `result-{agent-id}.md`에 Status: `quota_exceeded` 기록
|
|
92
|
+
4. 남은 작업 목록을 명시
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Serena Memory 접근 불가
|
|
97
|
+
|
|
98
|
+
1. 1회 재시도
|
|
99
|
+
2. 2회 연속 실패: 로컬 파일 `/tmp/progress-{agent-id}.md` 사용
|
|
100
|
+
3. result에 `memory_fallback: true` 플래그 추가
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 일반 원칙
|
|
105
|
+
|
|
106
|
+
- **3회 실패**: 같은 접근 3번 실패하면 반드시 다른 방법 시도
|
|
107
|
+
- **막힘**: 5턴 이상 진전 없으면 현재 상태 저장하고 `Status: blocked` 기록
|
|
108
|
+
- **범위 초과**: backend 문제 발견 시 result에 기록만, 직접 수정하지 말 것
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Frontend Agent - Examples
|
|
2
|
+
|
|
3
|
+
## Example 1: Feature Component
|
|
4
|
+
|
|
5
|
+
**Input**: "Create a todo list page with add, toggle, and delete"
|
|
6
|
+
|
|
7
|
+
**Output**:
|
|
8
|
+
```
|
|
9
|
+
## Task: Todo List Page
|
|
10
|
+
|
|
11
|
+
### Implementation
|
|
12
|
+
- Components: TodoList, TodoItem, AddTodoForm
|
|
13
|
+
- Routes: /todos (page)
|
|
14
|
+
- State: TanStack Query for server state, local state for form
|
|
15
|
+
|
|
16
|
+
### Files Created/Modified
|
|
17
|
+
- src/types/todo.ts (NEW) - Todo interface
|
|
18
|
+
- src/lib/api/todos.ts (NEW) - API client with TanStack Query hooks
|
|
19
|
+
- src/components/todo/TodoItem.tsx (NEW) - Single todo display
|
|
20
|
+
- src/components/todo/AddTodoForm.tsx (NEW) - Form with validation
|
|
21
|
+
- src/components/todo/TodoList.tsx (NEW) - List container
|
|
22
|
+
- src/app/todos/page.tsx (NEW) - Page component
|
|
23
|
+
- tests/components/TodoList.test.tsx (NEW)
|
|
24
|
+
|
|
25
|
+
### Testing
|
|
26
|
+
- Unit tests: 5 passing
|
|
27
|
+
- Keyboard navigation: verified
|
|
28
|
+
- Responsive: tested at 4 breakpoints
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example 2: Form with Validation
|
|
32
|
+
|
|
33
|
+
**Input**: "Create a user profile edit form"
|
|
34
|
+
|
|
35
|
+
**Output**:
|
|
36
|
+
```
|
|
37
|
+
## Task: Profile Edit Form
|
|
38
|
+
|
|
39
|
+
### Implementation
|
|
40
|
+
- Components: ProfileForm (React Hook Form + Zod)
|
|
41
|
+
- Validation: name (required, 2-50 chars), email (valid format), bio (optional, max 500)
|
|
42
|
+
- API: PATCH /api/users/me via TanStack Query mutation
|
|
43
|
+
- UX: optimistic update, error toast, success redirect
|
|
44
|
+
|
|
45
|
+
### Files Created
|
|
46
|
+
- src/types/user.ts
|
|
47
|
+
- src/lib/api/users.ts
|
|
48
|
+
- src/components/profile/ProfileForm.tsx
|
|
49
|
+
- src/app/profile/edit/page.tsx
|
|
50
|
+
- tests/components/ProfileForm.test.tsx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Example 3: Responsive Dashboard
|
|
54
|
+
|
|
55
|
+
**Input**: "Build a dashboard with stats cards and a chart"
|
|
56
|
+
|
|
57
|
+
**Output**:
|
|
58
|
+
```
|
|
59
|
+
## Task: Dashboard Page
|
|
60
|
+
|
|
61
|
+
### Implementation
|
|
62
|
+
- Components: StatCard, StatsGrid, ActivityChart
|
|
63
|
+
- Layout: CSS Grid - 1 col (mobile), 2 col (tablet), 4 col (desktop)
|
|
64
|
+
- Chart: recharts with responsive container
|
|
65
|
+
- Data: TanStack Query with 30s refetch interval
|
|
66
|
+
|
|
67
|
+
### Accessibility
|
|
68
|
+
- [x] All cards have aria-label with stat value
|
|
69
|
+
- [x] Chart has aria-describedby with text summary
|
|
70
|
+
- [x] Color contrast 4.5:1 on all text
|
|
71
|
+
|
|
72
|
+
### Files Created
|
|
73
|
+
- src/components/dashboard/StatCard.tsx
|
|
74
|
+
- src/components/dashboard/StatsGrid.tsx
|
|
75
|
+
- src/components/dashboard/ActivityChart.tsx
|
|
76
|
+
- src/app/dashboard/page.tsx
|
|
77
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Frontend Agent - Execution Protocol
|
|
2
|
+
|
|
3
|
+
## Step 0: Prepare
|
|
4
|
+
1. **Assess difficulty** — see `../_shared/difficulty-guide.md`
|
|
5
|
+
- **Simple**: Skip to Step 3 | **Medium**: All 4 steps | **Complex**: All steps + checkpoints
|
|
6
|
+
2. **Check lessons** — read your domain section in `../_shared/lessons-learned.md`
|
|
7
|
+
3. **Clarify requirements** — follow `../_shared/clarification-protocol.md`
|
|
8
|
+
- Check **Uncertainty Triggers**: 비즈니스 로직, 보안/인증, 기존 코드 충돌?
|
|
9
|
+
- Determine level: LOW → proceed | MEDIUM → present options | HIGH → ask immediately
|
|
10
|
+
4. **Budget context** — follow `../_shared/context-budget.md` (read symbols, not whole files)
|
|
11
|
+
|
|
12
|
+
**⚠️ Intelligent Escalation**: When uncertain, escalate early. Don't blindly proceed.
|
|
13
|
+
|
|
14
|
+
Follow these steps in order (adjust depth by difficulty).
|
|
15
|
+
|
|
16
|
+
## Step 1: Analyze
|
|
17
|
+
- Read the task requirements carefully
|
|
18
|
+
- Identify which components, pages, and hooks are needed
|
|
19
|
+
- Check existing code with Serena: `get_symbols_overview("src/components")`, `find_symbol("ComponentName")`
|
|
20
|
+
- Review existing patterns: `find_referencing_symbols("Button")` to understand usage conventions
|
|
21
|
+
- List assumptions; ask if unclear
|
|
22
|
+
|
|
23
|
+
## Step 2: Plan
|
|
24
|
+
- Decide on component structure (which are new, which extend existing)
|
|
25
|
+
- Define props interfaces with TypeScript
|
|
26
|
+
- Plan state management approach (local state, Context, Zustand)
|
|
27
|
+
- Identify API integration points (TanStack Query hooks)
|
|
28
|
+
- Plan responsive breakpoints and accessibility requirements
|
|
29
|
+
|
|
30
|
+
## Step 3: Implement
|
|
31
|
+
- Create/modify files in this order:
|
|
32
|
+
1. TypeScript types/interfaces
|
|
33
|
+
2. API client hooks (TanStack Query)
|
|
34
|
+
3. Reusable UI components (shadcn/ui based)
|
|
35
|
+
4. Feature components (compose UI + logic)
|
|
36
|
+
5. Page components (route-level)
|
|
37
|
+
6. Tests (unit + integration)
|
|
38
|
+
- Use `resources/component-template.tsx` as reference
|
|
39
|
+
- Follow `resources/tailwind-rules.md` for styling
|
|
40
|
+
|
|
41
|
+
## Step 4: Verify
|
|
42
|
+
- Run `resources/checklist.md` items
|
|
43
|
+
- Run `../_shared/common-checklist.md` items
|
|
44
|
+
- Check TypeScript strict mode: no errors
|
|
45
|
+
- Verify responsive design at 320px, 768px, 1024px, 1440px
|
|
46
|
+
- Test keyboard navigation and screen reader compatibility
|
|
47
|
+
|
|
48
|
+
## On Error
|
|
49
|
+
See `resources/error-playbook.md` for recovery steps.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Frontend Agent - Code Snippets
|
|
2
|
+
|
|
3
|
+
Copy-paste ready patterns. Use these as starting points, adapt to the specific task.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## React Component with Props
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
interface CardProps {
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Card({ title, description, onClick }: CardProps) {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
className="rounded-lg border bg-card p-4 text-left shadow-sm transition-colors hover:bg-accent"
|
|
21
|
+
>
|
|
22
|
+
<h3 className="text-lg font-semibold">{title}</h3>
|
|
23
|
+
{description && (
|
|
24
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
25
|
+
)}
|
|
26
|
+
</button>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## TanStack Query Hook
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
37
|
+
|
|
38
|
+
interface Todo {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
completed: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useTodos() {
|
|
45
|
+
return useQuery<Todo[]>({
|
|
46
|
+
queryKey: ["todos"],
|
|
47
|
+
queryFn: async () => {
|
|
48
|
+
const res = await fetch("/api/todos", {
|
|
49
|
+
headers: { Authorization: `Bearer ${getToken()}` },
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) throw new Error("Failed to fetch");
|
|
52
|
+
return res.json();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useCreateTodo() {
|
|
58
|
+
const queryClient = useQueryClient();
|
|
59
|
+
return useMutation({
|
|
60
|
+
mutationFn: async (data: { title: string }) => {
|
|
61
|
+
const res = await fetch("/api/todos", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
Authorization: `Bearer ${getToken()}`,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify(data),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) throw new Error("Failed to create");
|
|
70
|
+
return res.json();
|
|
71
|
+
},
|
|
72
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Form with React Hook Form + Zod
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
"use client";
|
|
83
|
+
|
|
84
|
+
import { useForm } from "react-hook-form";
|
|
85
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
86
|
+
import { z } from "zod";
|
|
87
|
+
|
|
88
|
+
const schema = z.object({
|
|
89
|
+
email: z.string().email("Invalid email"),
|
|
90
|
+
password: z.string().min(8, "At least 8 characters"),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
type FormData = z.infer<typeof schema>;
|
|
94
|
+
|
|
95
|
+
export function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
|
|
96
|
+
const {
|
|
97
|
+
register,
|
|
98
|
+
handleSubmit,
|
|
99
|
+
formState: { errors, isSubmitting },
|
|
100
|
+
} = useForm<FormData>({ resolver: zodResolver(schema) });
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
104
|
+
<div>
|
|
105
|
+
<label htmlFor="email" className="text-sm font-medium">
|
|
106
|
+
Email
|
|
107
|
+
</label>
|
|
108
|
+
<input
|
|
109
|
+
id="email"
|
|
110
|
+
type="email"
|
|
111
|
+
{...register("email")}
|
|
112
|
+
className="mt-1 w-full rounded-md border px-3 py-2"
|
|
113
|
+
aria-invalid={!!errors.email}
|
|
114
|
+
/>
|
|
115
|
+
{errors.email && (
|
|
116
|
+
<p className="mt-1 text-sm text-destructive">{errors.email.message}</p>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<label htmlFor="password" className="text-sm font-medium">
|
|
121
|
+
Password
|
|
122
|
+
</label>
|
|
123
|
+
<input
|
|
124
|
+
id="password"
|
|
125
|
+
type="password"
|
|
126
|
+
{...register("password")}
|
|
127
|
+
className="mt-1 w-full rounded-md border px-3 py-2"
|
|
128
|
+
aria-invalid={!!errors.password}
|
|
129
|
+
/>
|
|
130
|
+
{errors.password && (
|
|
131
|
+
<p className="mt-1 text-sm text-destructive">{errors.password.message}</p>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
<button
|
|
135
|
+
type="submit"
|
|
136
|
+
disabled={isSubmitting}
|
|
137
|
+
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
|
|
138
|
+
>
|
|
139
|
+
{isSubmitting ? "Signing in..." : "Sign in"}
|
|
140
|
+
</button>
|
|
141
|
+
</form>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Loading / Error / Empty States
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
interface AsyncStateProps<T> {
|
|
152
|
+
data: T | undefined;
|
|
153
|
+
isLoading: boolean;
|
|
154
|
+
error: Error | null;
|
|
155
|
+
empty: React.ReactNode;
|
|
156
|
+
children: (data: T) => React.ReactNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function AsyncState<T>({ data, isLoading, error, empty, children }: AsyncStateProps<T>) {
|
|
160
|
+
if (isLoading) return <div className="flex justify-center p-8"><Spinner /></div>;
|
|
161
|
+
if (error) return <ErrorCard message={error.message} />;
|
|
162
|
+
if (!data || (Array.isArray(data) && data.length === 0)) return <>{empty}</>;
|
|
163
|
+
return <>{children(data)}</>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Responsive Grid Layout
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
export function StatsGrid({ children }: { children: React.ReactNode }) {
|
|
173
|
+
return (
|
|
174
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Vitest Component Test
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { render, screen } from "@testing-library/react";
|
|
187
|
+
import userEvent from "@testing-library/user-event";
|
|
188
|
+
import { describe, it, expect, vi } from "vitest";
|
|
189
|
+
import { Card } from "./Card";
|
|
190
|
+
|
|
191
|
+
describe("Card", () => {
|
|
192
|
+
it("renders title and description", () => {
|
|
193
|
+
render(<Card title="Test" description="Desc" />);
|
|
194
|
+
expect(screen.getByText("Test")).toBeInTheDocument();
|
|
195
|
+
expect(screen.getByText("Desc")).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("calls onClick when clicked", async () => {
|
|
199
|
+
const onClick = vi.fn();
|
|
200
|
+
render(<Card title="Test" onClick={onClick} />);
|
|
201
|
+
await userEvent.click(screen.getByText("Test"));
|
|
202
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|