forked from cardosofelipe/fast-next-template
- 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>
206 lines
6.2 KiB
TypeScript
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,
|
|
});
|
|
}
|