关于 recovery-coach-patterns
面向开发 Recovery Coach(一款 Next.js 应用)的开发者,说明项目约定,涵盖组件、API 路由、Drizzle ORM 查询、样式、测试与错误处理。
主要功能
- 01Next.js 项目组织模式
- 02API 路由约定
- 03Drizzle ORM 查询模式
- 04组件样式指南
- 05测试与错误处理
适用场景
- 01为 Recovery Coach 添加页面或 API 路由
- 02使用 Drizzle ORM 编写数据库查询
- 03让组件符合项目设计系统
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill recovery-coach-patterns安装后在 Claude Code 中输入/recovery-coach-patterns确认已加载。
详细介绍
Recovery Coach 开发模式
此技能可帮助你遵循 Recovery Coach 代码库中既有的模式和约定。
适用场景
✅ 在以下场景使用此技能:
- 在 Recovery Coach 中编写新的组件、页面或 API 路由
- 遵循既有的代码组织模式
- 使用 Drizzle ORM 实现数据库查询
- 了解项目架构和约定
- 为组件设置样式,使其符合设计系统
❌ 请勿用于以下场景:
- 实现危机干预 → 使用
crisis-response-protocol - 一般的 Next.js 问题 → 使用 Next.js 文档
- AI/LLM 集成模式 → 使用
modern-drug-rehab-computer - 内容审核 → 使用
recovery-community-moderator
项目结构
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes (REST endpoints)
│ │ ├── auth/ # Authentication endpoints
│ │ ├── check-in/ # Daily check-in endpoints
│ │ ├── chat/ # AI coaching endpoints
│ │ └── admin/ # Admin-only endpoints
│ ├── admin/ # Admin dashboard page
│ ├── settings/ # User settings page
│ └── page.tsx # Home page
├── components/ # React components
│ ├── ui/ # Base UI components (Button, Card, etc.)
│ └── *.tsx # Feature components
├── lib/ # Core business logic
│ ├── ai/ # Anthropic integration
│ ├── hipaa/ # HIPAA compliance utilities
│ ├── auth.ts # Authentication
│ ├── db.ts # Database connection
│ └── rate-limit.ts # Rate limiting
├── db/ # Database schema (Drizzle ORM)
│ ├── schema.ts # Table definitions
│ └── secure-db.ts # RLS-enforced queries
└── test/ # Test utilities
features/ # Feature manifests (YAML)
docs/ # Documentation
scripts/ # Build and utility scripts
API 路由模式
// src/app/api/[feature]/route.ts
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { createRateLimiter } from '@/lib/rate-limit';
import { logPHIAccess } from '@/lib/hipaa/audit';
import { z } from 'zod';
// 1. Define rate limiter
const rateLimiter = createRateLimiter({
windowMs: 60000,
maxRequests: 30,
keyPrefix: 'api:feature'
});
// 2. Define input schema
const RequestSchema = z.object({
field: z.string().min(1).max(1000),
});
export async function POST(request: Request) {
// 3. Check authentication
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 4. Apply rate limiting
const rateLimitResult = await rateLimiter.check(session.userId);
if (!rateLimitResult.allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429, headers: rateLimitResult.headers }
);
}
// 5. Parse and validate input
const body = await request.json();
const parsed = RequestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Invalid input', details: parsed.error.issues },
{ status: 400 }
);
}
// 6. Perform operation
const result = await performOperation(session.userId, parsed.data);
// 7. Audit log (if PHI)
await logPHIAccess(session.userId, 'feature', result.id, 'CREATE');
// 8. Return response
return NextResponse.json(result);
}
React 组件模式
// src/components/FeatureComponent.tsx
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
interface FeatureProps {
id: string;
initialData?: FeatureData;
onComplete?: (result: Result) => void;
}
export function FeatureComponent({
id,
initialData,
onComplete
}: FeatureProps) {
const [data, setData] = useState<FeatureData | null>(initialData ?? null);
const [loading, setLoading] = useState(!initialData);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!initialData) {
fetchData();
}
}, [id]);
async function fetchData() {
try {
setLoading(true);
const res = await fetch(`/api/feature/${id}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setData(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
}
if (loading) {
return <div className="animate-pulse">Loading...</div>;
}
if (error) {
return (
<Card className="border-destructive">
<CardContent>Error: {error}</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>Feature Title</CardHeader>
<CardContent>
{/* Content */}
</CardContent>
</Card>
);
}
数据库查询模式
// Use secure-db for user data (RLS enforced)
import { db, users, checkIns } from '@/db/secure-db';
import { eq, desc } from 'drizzle-orm';
// Get user's own data (RLS automatically filters)
async function getUserCheckIns(userId: string) {
return db
.select()
.from(checkIns)
.where(eq(checkIns.userId, userId))
.orderBy(desc(checkIns.createdAt))
.limit(30);
}
// For admin queries, use requireAdmin
import { requireAdmin } from '@/db/secure-db';
async function getAdminStats() {
const admin = await requireAdmin();
if (!admin) throw new Error('Admin required');
// Now can query across all users
return db.select({ count: count() }).from(users);
}
设计系统
色彩方案(疗愈)
/* From globals.css */
--navy: #1a365d; /* Primary - trust, stability */
--teal: #319795; /* Secondary - calm, healing */
--coral: #ed8936; /* Accent - warmth, energy */
--cream: #fffaf0; /* Background - comfort */
基于时间的主题
function getTimeTheme(): 'dawn' | 'day' | 'dusk' | 'night' {
const hour = new Date().getHours();
if (hour >= 5 && hour < 9) return 'dawn';
if (hour >= 9 && hour < 17) return 'day';
if (hour >= 17 && hour < 21) return 'dusk';
return 'night';
}
组件样式
// Use Tailwind with design tokens
<Button
className="bg-navy hover:bg-navy/90 text-white"
variant="default"
>
Primary Action
</Button>
<Card className="bg-cream border-teal/20">
<CardContent className="text-navy">
Therapeutic content
</CardContent>
</Card>
测试模式
// src/lib/__tests__/feature.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Feature', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('loads and displays data', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: 'test' })
} as Response);
render(<FeatureComponent id="123" />);
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
});
it('handles errors gracefully', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
render(<FeatureComponent id="123" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
错误处理
// Use structured error responses
interface APIError {
error: string;
code?: string;
details?: unknown;
}
// In API routes
return NextResponse.json<APIError>(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: zodError.issues
},
{ status: 400 }
);
// In components
try {
await submitData();
} catch (e) {
if (e instanceof APIError) {
toast.error(e.message);
} else {
toast.error('An unexpected error occurred');
console.error(e); // Log for debugging, not shown to user
}
}
环境变量
必需变量(启动时验证):
# Authentication
SESSION_SECRET= # 32+ random characters
# AI Integration
ANTHROPIC_API_KEY= # Claude API key
# Database
DATABASE_URL= # SQLite path or connection string
# Optional
VAPID_PUBLIC_KEY= # Push notifications
VAPID_PRIVATE_KEY=
提交前检查清单
提交任何更改之前:
-
npm run lint通过 -
npm run test通过 -
npm run feature:validate通过 - 已更新功能清单(如适用)
- 代码中不包含机密信息
- 保持符合 HIPAA 合规要求(审计日志)
- 考虑无障碍性(语义化 HTML、ARIA)
常用导入
// Authentication
import { getSession, requireAuth } from '@/lib/auth';
// Database
import { db } from '@/db/secure-db';
import { eq, desc, and } from 'drizzle-orm';
// Audit
import { logPHIAccess, logSecurityEvent } from '@/lib/hipaa/audit';
// Rate limiting
import { createRateLimiter } from '@/lib/rate-limit';
// Validation
import { z } from 'zod';
// UI Components
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
评论
支持 Markdown