Files
syndarix/frontend/src/lib/api/hooks/useProjects.ts
Felipe Cardoso 731a188a76 feat(frontend): wire useProjects hook to SDK and enhance MSW handlers
- Regenerate API SDK with 77 endpoints (up from 61)
- Update useProjects hook to use SDK's listProjects function
- Add comprehensive project mock data for demo mode
- Add project CRUD handlers to MSW overrides
- Map API response to frontend ProjectListItem format
- Fix test files with required slug and autonomyLevel properties

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 02:22:44 +01:00

206 lines
6.2 KiB
TypeScript

/**
* Projects List Hook
*
* Provides data for the projects list page with filtering,
* sorting, and pagination.
*
* Uses SDK to call API (intercepted by MSW in demo mode).
*
* @see Issue #54
*/
import { useQuery } from '@tanstack/react-query';
import { listProjects as listProjectsApi } from '@/lib/api/generated';
import type { ProjectStatus as ApiProjectStatus, ProjectResponse } from '@/lib/api/generated';
import type { ProjectStatus } from '@/components/projects/types';
// ============================================================================
// Types
// ============================================================================
export interface ProjectListItem {
id: string;
name: string;
slug: string;
description?: string;
status: ProjectStatus;
complexity: 'low' | 'medium' | 'high';
progress: number;
openIssues: number;
activeAgents: number;
currentSprint?: string;
lastActivity: string;
createdAt: string;
owner: {
id: string;
name: string;
};
tags?: string[];
autonomyLevel: string;
}
export interface ProjectsListParams {
search?: string;
status?: ProjectStatus | 'all';
complexity?: 'low' | 'medium' | 'high' | 'all';
sortBy?: 'recent' | 'name' | 'progress' | 'issues';
sortOrder?: 'asc' | 'desc';
page?: number;
limit?: number;
}
export interface ProjectsListResponse {
data: ProjectListItem[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// ============================================================================
// Helpers
// ============================================================================
/**
* Maps API ProjectResponse to our ProjectListItem format
*/
function mapProjectResponse(project: ProjectResponse & Record<string, unknown>): ProjectListItem {
// Map complexity from mock data format to UI format
const rawComplexity = project.complexity as string;
let complexity: 'low' | 'medium' | 'high' = 'medium';
if (rawComplexity === 'script' || rawComplexity === 'simple' || rawComplexity === 'low') {
complexity = 'low';
} else if (rawComplexity === 'complex' || rawComplexity === 'high') {
complexity = 'high';
}
return {
id: project.id,
name: project.name,
slug: project.slug || project.name.toLowerCase().replace(/\s+/g, '-'),
description: project.description || undefined,
status: project.status as ProjectStatus,
complexity,
progress: (project.progress as number) || 0,
openIssues: (project.openIssues as number) || project.issue_count || 0,
activeAgents: (project.activeAgents as number) || project.agent_count || 0,
currentSprint: project.active_sprint_name || undefined,
lastActivity: (project.lastActivity as string) || formatRelativeTime(project.updated_at),
createdAt: project.created_at,
owner: {
id: project.owner_id || 'unknown',
name: (project.ownerName as string) || 'Unknown',
},
tags: (project.tags as string[]) || [],
autonomyLevel: project.autonomy_level || 'milestone',
};
}
/**
* Format a date string as relative time (e.g., "2 minutes ago")
*/
function formatRelativeTime(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`;
return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
}
// ============================================================================
// Hook
// ============================================================================
/**
* Fetches projects list with filtering, sorting, and pagination
*/
export function useProjects(params: ProjectsListParams = {}) {
const {
search = '',
status = 'all',
complexity = 'all',
sortBy = 'recent',
sortOrder = 'desc',
page = 1,
limit = 50,
} = params;
return useQuery<ProjectsListResponse>({
queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }],
queryFn: async () => {
// Call API via SDK (MSW intercepts in demo mode)
const response = await listProjectsApi({
query: {
status: status !== 'all' ? (status as ApiProjectStatus) : undefined,
search: search || undefined,
page,
limit,
},
});
if (response.error) {
throw new Error('Failed to fetch projects');
}
// Get raw response data
const apiData = response.data;
let projects = apiData.data.map((p) =>
mapProjectResponse(p as ProjectResponse & Record<string, unknown>)
);
// Client-side complexity filter (not supported by API)
if (complexity !== 'all') {
projects = projects.filter((p) => p.complexity === complexity);
}
// Client-side sorting
projects.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name);
break;
case 'progress':
comparison = a.progress - b.progress;
break;
case 'issues':
comparison = a.openIssues - b.openIssues;
break;
case 'recent':
default:
comparison = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
break;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
// Calculate pagination
const total = apiData.pagination.total;
const totalPages = Math.ceil(total / limit);
return {
data: projects,
pagination: {
page,
limit,
total,
totalPages,
},
};
},
staleTime: 30000,
});
}