CLAUDE.md · git:20260127.6a920a7 · 2026-01-27 · sha256 0dec67ab4fc1915d

CLAUDE.md git:20260127.6a920a7A

Immutable. This exact content is served forever at /api/v1/blob/0dec67ab4fc1915d.

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

PromptMinder is a Next.js 16 application for managing and sharing AI prompts. It features team-based multi-tenancy, public prompt sharing, and a contribution system for community prompts.

**Core Stack:**
- Next.js 16 (App Router)
- React 19 (functional components with hooks)
- Clerk for authentication
- Supabase for database
- Radix UI for accessible primitives
- Tailwind CSS for styling
- No TypeScript - uses JavaScript with jsconfig.json

## Commands

Use **pnpm** for all package management:

### Development
- `pnpm dev` - Start development server (http://localhost:3000)
- `pnpm build` - Build for production
- `pnpm start` - Start production server
- `pnpm lint` - Run ESLint (must pass after changes)

### Testing
- `pnpm test` - Run all tests
- `pnpm test:watch` - Run tests in watch mode
- `pnpm test:coverage` - Run tests with coverage report
- `pnpm test __tests__/path/to/test.test.js` - Run specific test file
- `pnpm test --testNamePattern="test description"` - Run tests matching pattern

### Analysis
- `pnpm analyze` - Build with bundle analyzer
- `pnpm css:optimize` - Optimize CSS for production

## Multi-Tenant Team Architecture

This is a multi-tenant application where all data (prompts, tags, etc.) belongs to a team. Understanding the team system is critical.

### Team Model

**Personal vs. Team Workspaces:**
- Every user has an auto-generated "personal team" (`is_personal: true`)
- Users can create up to 2 additional non-personal teams
- Personal teams cannot have members (owner only)
- Non-personal teams support invitations and role-based permissions

**Team Roles:**
- `owner` - Full control, can transfer ownership
- `admin` - Can manage members and update team settings
- `member` - Can access team resources but not manage

**Membership Statuses:**
- `active` - Full member access
- `pending` - Invited but not yet accepted
- `left`, `removed`, `blocked` - Inactive states

### Team Context Flow

**Client-side:**
1. `TeamContext` (contexts/team-context.js) manages active team selection
2. Active team ID stored in localStorage via `TEAM_STORAGE_KEY`
3. `null` team ID represents personal workspace
4. `ApiClient` reads from localStorage and sends `X-Team-Id` header

**Server-side (API routes):**
1. Extract team ID from `X-Team-Id` header or `?teamId=` query param via `extractTeamId()`
2. Call `resolveTeamContext()` to get supabase client, teamService, and membership
3. Use `teamService.requireMembership()` to authorize access
4. Pass `teamId` to database queries

**Example API route pattern:**
```javascript
export async function GET(request) {
  const userId = await requireUserId()
  const { teamId, supabase, teamService } = await resolveTeamContext(request, userId, {
    requireMembership: true,  // Verify user is a team member
    allowMissingTeam: false   // Require teamId to be present
  })

  // Query data with team_id filter
  const { data } = await supabase
    .from('prompts')
    .select('*')
    .eq('team_id', teamId)

  return NextResponse.json({ prompts: data })
}
```

**For operations that work across personal and team contexts:**
```javascript
const { teamId, supabase, teamService } = await resolveTeamContext(request, userId, {
  requireMembership: false,
  allowMissingTeam: true
})

const query = teamId
  ? supabase.from('prompts').select('*').eq('team_id', teamId)
  : supabase.from('prompts').select('*').is('team_id', null)
```

### Key Team Files

- `lib/team-service.js` - Business logic for team operations (invite, accept, leave, transfer ownership)
- `lib/team-request.js` - Server-side team context resolution from headers/params
- `lib/team-storage.js` - Constants and utilities for localStorage team persistence
- `contexts/team-context.js` - Client-side team state management
- `components/team/TeamSwitcher.jsx` - UI for switching between teams

## Authentication & Authorization

**Clerk handles authentication**, `requireUserId()` from `@/lib/auth.js` gets the current user ID.

**Authorization is team-based:**
- Use `teamService.requireMembership()` to check if user can access a resource
- Use `teamService.assertManager()` for admin/owner-only operations
- Use `teamService.assertOwner()` for owner-only operations

## Data Layer Patterns

### API Routes (app/api/**)

**Standard pattern:**
1. Get userId via `requireUserId()`
2. Resolve team context via `resolveTeamContext()`
3. Validate membership if needed
4. Perform Supabase queries
5. Handle errors with `handleApiError()`

**Team-aware queries:**
- All data tables should have a `team_id` column (nullable for personal items)
- Always filter by `team_id` in WHERE clauses
- For personal workspace items, use `.is('team_id', null)`

### Client Components

**Use centralized API client:**
```javascript
import { apiClient } from '@/lib/api-client'

// Get prompts for active team
const data = await apiClient.getPrompts({ tag: 'chatbot' })

// Create prompt in specific team
const newPrompt = await apiClient.createPrompt(promptData, { teamId: 'xxx' })
```

**ApiClient automatically:**
- Reads team ID from localStorage
- Adds `X-Team-Id` header to requests
- Handles JSON serialization
- Throws ApiError with status and details

### Custom Hooks

**Data fetching hooks** (`hooks/use-*.js`):
- `usePrompts(filters)` - Fetch and manage prompts
- `usePromptDetail(id)` - Fetch single prompt
- `useTeam()` - Access team context from TeamProvider

**Utility hooks:**
- `useToast()` - Show notifications
- `usePerformance()` - Performance monitoring

## Code Style

### Imports
- Use absolute imports with `@/` alias: `@/components/...`, `@/lib/...`
- External libraries first, then internal modules
- Example: `import { apiClient } from '@/lib/api-client'`

### File Types
- `.jsx` - React components (add `'use client'` directive for client components)
- `.js` - Utilities, hooks, API routes, tests
- No TypeScript

### Naming Conventions
- Components: PascalCase (PromptCard, Button)
- Functions: camelCase (fetchPrompts, handleCopy)
- Constants: UPPER_SNAKE_CASE (PERSONAL_TEAM_ID, DEFAULTS)
- Hooks: use prefix (usePrompts, useToast)
- Files: kebab-case (prompt-card.jsx, api-client.js)

### React Patterns
- Functional components with hooks
- Destructure props in signature: `function PromptCard({ prompt, onUpdate }) { ... }`
- Memoize with `useMemo` and `useCallback` for performance
- Use `cn()` utility (from `@/lib/utils`) for className merging

### Error Handling
- Use ApiError class for API errors
- Wrap async operations in try-catch
- Show user feedback with useToast hook
- Log errors: `console.error('Error fetching prompts:', error)`

## Database Schema Patterns

**Team-based tables** (prompts, tags, etc.):
- `team_id` (uuid, nullable) - Foreign key to teams table
- `created_by` (uuid) - User who created the record
- `created_at`, `updated_at` (timestamptz)

**Team membership table** (team_members):
- Composite uniqueness on (team_id, user_id)
- Only one `owner` per team
- Stores invitation status and role

## Public vs. Private Prompts

**is_public flag:**
- `true` - Prompt appears in public gallery and contribution system
- `false` - Prompt only visible to team members

**Public prompts API:**
- `GET /api/prompts/public` - List public prompts (no auth required)
- Admin can moderate via `/api/admin/public-prompts`

## Testing

- Tests in `__tests__/` directory mirror source structure
- Jest with React Testing Library
- Use Chinese descriptions: `it('应该正确渲染基本按钮', () => ...)`
- Mock fetch: `global.fetch = jest.fn()` and `fetch.mockClear()` in beforeEach
- Coverage threshold: 70%
- Run `pnpm test:watch` during development