Markdown Converter
Agent skill for markdown-converter
Feature-based architecture for organizing frontend functionality. Each feature is a self-contained module with its own components, hooks, and state management.
Sign in to like and favorite skills
Feature-based architecture for organizing frontend functionality. Each feature is a self-contained module with its own components, hooks, and state management.
Organize frontend code by business feature rather than by type. This approach:
features/ └── auth/ # Authentication feature module ├── LoginForm.tsx # Login UI component ├── SignupForm.tsx # Signup UI component ├── hooks.ts # TanStack Query hooks ├── index.ts # Barrel export └── (types.ts) # Optional: TypeScript types
auth - AuthenticationPurpose: User login and signup functionality.
Files:
LoginForm.tsx - Email/password login form with validationSignupForm.tsx - User registration formhooks.ts - TanStack Query mutations:
useLogin() - Authenticate useruseSignup() - Register new useruseCurrentUser() - Fetch current logged-in userKey Features:
Usage Example:
import { useLogin } from '@/features/auth' export function MyComponent() { const mutation = useLogin() const handleLogin = (email: string, password: string) => { mutation.mutate({ email, password }) } return <form onSubmit={handleLogin}>...</form> }
Follow this structure when creating a new feature:
mkdir -p src/features/myfeature
// src/features/myfeature/MyComponent.tsx import { useMyFeature } from './hooks' export function MyComponent() { const data = useMyFeature() return <div>{data.name}</div> }
// src/features/myfeature/hooks.ts import { useMutation, useQuery } from '@tanstack/react-query' import { api } from '@/lib/api' export const useMyFeature = () => useQuery({ queryKey: ['myfeature'], queryFn: () => api.myfeature.get(), }) export const useCreateItem = () => useMutation({ mutationFn: (data: CreateItemInput) => api.myfeature.post(data), })
// src/features/myfeature/index.ts export { MyComponent } from './MyComponent' export * from './hooks'
// src/App.tsx or any component import { MyComponent, useMyFeature } from '@/features/myfeature'
index.ts to control public API// ✅ Good: Descriptive query key useQuery({ queryKey: ['users', userId, 'posts'], queryFn: () => api.users.getById(userId).posts(), }) // ❌ Bad: Generic query key useQuery({ queryKey: ['data'], queryFn: () => api.getData(), })
// ✅ Good: Focused, reusable component export function LoginForm({ onSuccess }: { onSuccess: () => void }) { const mutation = useLogin() // ... } // ❌ Bad: Component mixing multiple concerns export function Page() { // Authentication, form, error handling, API calls, UI all mixed }
// ✅ Good: Server state with Query const { data: user, isLoading } = useQuery({ queryKey: ['currentUser'], queryFn: () => api.users.getCurrentUser(), }) // ✅ Good: UI state with useState const [isModalOpen, setIsModalOpen] = useState(false) // ❌ Avoid: Global state when local is sufficient const [name, setName] = useGlobalState('name') // Unless shared across many components
Features should not depend on other features
auth/hooks.ts importing from dashboard//lib or /componentsFeatures can depend on:
/lib - Shared utilities and API client/components - Shared UI components (if needed)export const useUserPosts = (userId: string) => { const query = useQuery({ queryKey: ['users', userId, 'posts'], queryFn: () => api.users.getById(userId).posts(), }) return { ...query, refetch: query.refetch, } }
export const useUpdatePost = () => useMutation({ mutationFn: (post: Post) => api.posts.update(post), onMutate: (newPost) => { // Optimistically update cache queryClient.setQueryData(['posts'], (old: Post[]) => old.map((p) => (p.id === newPost.id ? newPost : p)) ) }, onError: () => { // Revert on error queryClient.invalidateQueries({ queryKey: ['posts'] }) }, })
export function MyComponent() { const mutation = useCreateItem() if (mutation.isError) { return ( <div className="error"> {mutation.error?.message || 'An error occurred'} </div> ) } return ( <div> {mutation.isPending && <Spinner />} <form onSubmit={(e) => { e.preventDefault() mutation.mutate(data) }}> {/* Form content */} </form> </div> ) }
export function MyComponent() { const { data, isPending, error } = useMyFeature() if (isPending) return <Spinner /> if (error) return <Error message={error.message} /> return <div>{data?.name}</div> }
Each feature should include tests for:
// features/myfeature/__tests__/hooks.test.ts import { renderHook, waitFor } from '@testing-library/react' import { useMyFeature } from '../hooks' describe('useMyFeature', () => { it('fetches data successfully', async () => { const { result } = renderHook(() => useMyFeature()) await waitFor(() => { expect(result.current.isSuccess).toBe(true) }) expect(result.current.data).toBeDefined() }) })
CRITICAL: Every new feature MUST include Stagehand E2E tests before being considered complete.
When adding a new feature:
e2e-stagehand/tests/{feature}/{feature}.test.tsbun run test:e2e:stagehand// e2e-stagehand/tests/myfeature/myfeature.test.ts import 'dotenv/config' import { describe, test, expect, beforeAll, afterAll } from 'vitest' import { Stagehand } from '@browserbasehq/stagehand' import { z } from 'zod' import { APP_URL, createStagehand, delay } from '../../helpers/stagehand' describe('MyFeature E2E Tests', () => { let stagehand: Stagehand beforeAll(async () => { stagehand = await createStagehand() }) afterAll(async () => { await stagehand.close() }) test('should complete main user flow', async () => { const page = stagehand.context.pages()[0] await page.goto(`${APP_URL}/myfeature`) await delay(1500) // Use natural language for actions await stagehand.act('Click the "Create New" button') await stagehand.act('Type "My Item" in the name field') await stagehand.act('Click the submit button') await delay(2000) // Validate with Zod schema const result = await stagehand.extract( 'Check if the item was created successfully', z.object({ success: z.boolean(), itemName: z.string().optional(), }) ) expect(result.success).toBe(true) }) test('should show error for invalid input', async () => { const page = stagehand.context.pages()[0] await page.goto(`${APP_URL}/myfeature`) await delay(1500) await stagehand.act('Click the submit button without filling the form') await delay(1000) const result = await stagehand.extract( 'Check if there is a validation error', z.object({ hasError: z.boolean(), errorMessage: z.string().optional(), }) ) expect(result.hasError).toBe(true) }) })
# Run all Stagehand E2E tests bun run test:e2e:stagehand # Run with visible browser (for debugging) cd e2e-stagehand && HEADLESS=false bun run test:headed # Run in watch mode cd e2e-stagehand && bun run test:watch
See
for detailed Stagehand documentation.e2e-stagehand/CLAUDE.md
Potential features to add: