diff --git a/frontend/src/app/[locale]/(authenticated)/projects/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/page.tsx new file mode 100644 index 0000000..4f259b7 --- /dev/null +++ b/frontend/src/app/[locale]/(authenticated)/projects/page.tsx @@ -0,0 +1,145 @@ +/** + * Projects List Page + * + * Displays all projects with filtering, sorting, and search. + * Supports grid and list view modes. + * + * @see Issue #54 + */ + +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Link } from '@/lib/i18n/routing'; +import { ProjectFilters, ProjectsGrid } from '@/components/projects'; +import type { ViewMode, SortBy, SortOrder, Complexity } from '@/components/projects'; +import type { ProjectStatus } from '@/components/projects/types'; +import { useProjects, type ProjectListItem } from '@/lib/api/hooks/useProjects'; +import { useDebounce } from '@/lib/hooks/useDebounce'; + +export default function ProjectsPage() { + const router = useRouter(); + + // Filter state + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [complexityFilter, setComplexityFilter] = useState('all'); + const [sortBy, setSortBy] = useState('recent'); + const [sortOrder, setSortOrder] = useState('desc'); + const [viewMode, setViewMode] = useState('grid'); + + // Debounce search for API calls + const debouncedSearch = useDebounce(searchQuery, 300); + + // Fetch projects + const { data, isLoading, error } = useProjects({ + search: debouncedSearch || undefined, + status: statusFilter, + complexity: complexityFilter !== 'all' ? complexityFilter : undefined, + sortBy, + sortOrder, + page: 1, + limit: 50, + }); + + // Check if any filters are active (for empty state message) + const hasFilters = useMemo(() => { + return searchQuery !== '' || statusFilter !== 'all' || complexityFilter !== 'all'; + }, [searchQuery, statusFilter, complexityFilter]); + + // Handle project card click + const handleProjectClick = useCallback( + (project: ProjectListItem) => { + router.push(`/projects/${project.id}`); + }, + [router] + ); + + // Handle project action + const handleProjectAction = useCallback( + (project: ProjectListItem, action: 'archive' | 'pause' | 'resume' | 'delete') => { + // TODO: Implement actual API calls + switch (action) { + case 'archive': + toast.success(`Archived: ${project.name}`); + break; + case 'pause': + toast.info(`Paused: ${project.name}`); + break; + case 'resume': + toast.success(`Resumed: ${project.name}`); + break; + case 'delete': + toast.error(`Deleted: ${project.name}`); + break; + } + }, + [] + ); + + // Show error toast if fetch fails + if (error) { + toast.error('Failed to load projects', { + description: 'Please try again later', + }); + } + + return ( +
+ {/* Header */} +
+
+

Projects

+

Manage and monitor your projects

+
+ +
+ + {/* Filters */} + + + {/* Projects Grid */} + + + {/* Pagination - TODO: Add when more than 50 projects */} + {data && data.pagination.totalPages > 1 && ( +
+ + Showing {data.data.length} of {data.pagination.total} projects + + {/* TODO: Add pagination controls */} +
+ )} +
+ ); +} diff --git a/frontend/src/components/projects/ProjectCard.tsx b/frontend/src/components/projects/ProjectCard.tsx new file mode 100644 index 0000000..deb25fb --- /dev/null +++ b/frontend/src/components/projects/ProjectCard.tsx @@ -0,0 +1,199 @@ +/** + * ProjectCard Component + * + * Displays a project card for the projects grid view. + * Shows project status, progress, metrics, and quick info. + * + * @see Issue #54 + */ + +'use client'; + +import { Bot, CircleDot, Clock, MoreVertical, Archive, Play, Pause } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; +import { ProjectStatusBadge } from './StatusBadge'; +import { ProgressBar } from './ProgressBar'; +import type { ProjectListItem } from '@/lib/api/hooks/useProjects'; + +export interface ProjectCardProps { + /** Project data */ + project: ProjectListItem; + /** Called when card is clicked */ + onClick?: () => void; + /** Called when action menu item is selected */ + onAction?: (action: 'archive' | 'pause' | 'resume' | 'delete') => void; + /** Additional CSS classes */ + className?: string; +} + +/** + * Complexity indicator dots + */ +function ComplexityIndicator({ complexity }: { complexity: 'low' | 'medium' | 'high' }) { + const levels = { low: 1, medium: 2, high: 3 }; + const level = levels[complexity]; + + return ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ); +} + +/** + * Loading skeleton for project cards + */ +export function ProjectCardSkeleton() { + return ( + + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+
+ ); +} + +export function ProjectCard({ project, onClick, onAction, className }: ProjectCardProps) { + return ( + { + /* istanbul ignore next -- keyboard handler */ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick?.(); + } + }} + > + +
+ +
+ + {onAction && ( + + e.stopPropagation()}> + + + e.stopPropagation()}> + {project.status === 'paused' ? ( + onAction('resume')}> + + Resume Project + + ) : project.status === 'active' ? ( + onAction('pause')}> + + Pause Project + + ) : null} + onAction('archive')}> + + Archive Project + + + onAction('delete')} + > + Delete Project + + + + )} +
+
+ + {project.name} + + {project.description && ( +

{project.description}

+ )} +
+ + + + + {project.tags && project.tags.length > 0 && ( +
+ {project.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {project.tags.length > 3 && ( + + +{project.tags.length - 3} + + )} +
+ )} + +
+
+ + + {project.activeAgents} + + + + {project.openIssues} + +
+ + + {project.lastActivity} + +
+
+
+ ); +} diff --git a/frontend/src/components/projects/ProjectFilters.tsx b/frontend/src/components/projects/ProjectFilters.tsx new file mode 100644 index 0000000..c9c6309 --- /dev/null +++ b/frontend/src/components/projects/ProjectFilters.tsx @@ -0,0 +1,272 @@ +/** + * ProjectFilters Component + * + * Filter controls for the projects list including: + * - Search input with debounce + * - Status filter + * - Extended filters (complexity, etc.) + * - Sort controls + * - View mode toggle (grid/list) + * + * @see Issue #54 + */ + +'use client'; + +import { useState } from 'react'; +import { + Search, + Filter, + LayoutGrid, + List, + ChevronDown, + X, +} from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { cn } from '@/lib/utils'; +import type { ProjectStatus } from './types'; + +export type ViewMode = 'grid' | 'list'; +export type SortBy = 'recent' | 'name' | 'progress' | 'issues'; +export type SortOrder = 'asc' | 'desc'; +export type Complexity = 'low' | 'medium' | 'high' | 'all'; + +export interface ProjectFiltersProps { + /** Current search query */ + searchQuery: string; + /** Called when search changes */ + onSearchChange: (query: string) => void; + /** Current status filter */ + statusFilter: ProjectStatus | 'all'; + /** Called when status filter changes */ + onStatusFilterChange: (status: ProjectStatus | 'all') => void; + /** Current complexity filter */ + complexityFilter: Complexity; + /** Called when complexity filter changes */ + onComplexityFilterChange: (complexity: Complexity) => void; + /** Current sort field */ + sortBy: SortBy; + /** Called when sort field changes */ + onSortByChange: (sortBy: SortBy) => void; + /** Current sort order */ + sortOrder: SortOrder; + /** Called when sort order changes */ + onSortOrderChange: (order: SortOrder) => void; + /** Current view mode */ + viewMode: ViewMode; + /** Called when view mode changes */ + onViewModeChange: (mode: ViewMode) => void; + /** Additional CSS classes */ + className?: string; +} + +const statusOptions: { value: ProjectStatus | 'all'; label: string }[] = [ + { value: 'all', label: 'All Status' }, + { value: 'active', label: 'Active' }, + { value: 'paused', label: 'Paused' }, + { value: 'completed', label: 'Completed' }, + { value: 'archived', label: 'Archived' }, +]; + +const complexityOptions: { value: Complexity; label: string }[] = [ + { value: 'all', label: 'All Complexity' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, +]; + +const sortOptions: { value: SortBy; label: string }[] = [ + { value: 'recent', label: 'Most Recent' }, + { value: 'name', label: 'Name' }, + { value: 'progress', label: 'Progress' }, + { value: 'issues', label: 'Issues' }, +]; + +export function ProjectFilters({ + searchQuery, + onSearchChange, + statusFilter, + onStatusFilterChange, + complexityFilter, + onComplexityFilterChange, + sortBy, + onSortByChange, + sortOrder, + onSortOrderChange, + viewMode, + onViewModeChange, + className, +}: ProjectFiltersProps) { + const [showExtended, setShowExtended] = useState(false); + + // Check if any filters are active + const hasActiveFilters = + statusFilter !== 'all' || complexityFilter !== 'all' || searchQuery !== ''; + + // Count active filters for badge + const activeFilterCount = + (statusFilter !== 'all' ? 1 : 0) + + (complexityFilter !== 'all' ? 1 : 0) + + (searchQuery !== '' ? 1 : 0); + + const clearAllFilters = () => { + onSearchChange(''); + onStatusFilterChange('all'); + onComplexityFilterChange('all'); + }; + + return ( +
+ {/* Quick Filters Row */} +
+ {/* Search */} +
+ + onSearchChange(e.target.value)} + className="pl-9" + aria-label="Search projects" + /> +
+ + {/* Status Filter */} + + + {/* Extended Filters Toggle */} + + + {/* View Mode Toggle */} +
+ + +
+
+ + {/* Extended Filters */} + {showExtended && ( + +
+ {/* Complexity Filter */} +
+ + +
+ + {/* Sort By */} +
+ + +
+ + {/* Sort Order */} +
+ + +
+ + {/* Clear Filters */} + {hasActiveFilters && ( + + )} +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/projects/ProjectsGrid.tsx b/frontend/src/components/projects/ProjectsGrid.tsx new file mode 100644 index 0000000..03049e9 --- /dev/null +++ b/frontend/src/components/projects/ProjectsGrid.tsx @@ -0,0 +1,119 @@ +/** + * ProjectsGrid Component + * + * Displays projects in either grid or list view with + * loading and empty states. + * + * @see Issue #54 + */ + +'use client'; + +import { Folder, Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Link } from '@/lib/i18n/routing'; +import { cn } from '@/lib/utils'; +import { ProjectCard, ProjectCardSkeleton } from './ProjectCard'; +import type { ProjectListItem } from '@/lib/api/hooks/useProjects'; +import type { ViewMode } from './ProjectFilters'; + +export interface ProjectsGridProps { + /** Projects to display */ + projects: ProjectListItem[]; + /** Whether data is loading */ + isLoading?: boolean; + /** Current view mode */ + viewMode?: ViewMode; + /** Called when a project card is clicked */ + onProjectClick?: (project: ProjectListItem) => void; + /** Called when a project action is selected */ + onProjectAction?: (project: ProjectListItem, action: 'archive' | 'pause' | 'resume' | 'delete') => void; + /** Whether filters are currently applied (affects empty state message) */ + hasFilters?: boolean; + /** Additional CSS classes */ + className?: string; +} + +/** + * Empty state component + */ +function EmptyState({ hasFilters }: { hasFilters: boolean }) { + return ( +
+
+ +
+

No projects found

+

+ {hasFilters + ? 'Try adjusting your filters or search query' + : 'Get started by creating your first project'} +

+ {!hasFilters && ( + + )} +
+ ); +} + +/** + * Loading skeleton grid + */ +function LoadingSkeleton({ viewMode }: { viewMode: ViewMode }) { + return ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( + + ))} +
+ ); +} + +export function ProjectsGrid({ + projects, + isLoading = false, + viewMode = 'grid', + onProjectClick, + onProjectAction, + hasFilters = false, + className, +}: ProjectsGridProps) { + if (isLoading) { + return ; + } + + if (projects.length === 0) { + return ; + } + + return ( +
+ {projects.map((project) => ( + onProjectClick?.(project)} + onAction={onProjectAction ? (action) => onProjectAction(project, action) : undefined} + /> + ))} +
+ ); +} diff --git a/frontend/src/lib/api/hooks/useProjects.ts b/frontend/src/lib/api/hooks/useProjects.ts new file mode 100644 index 0000000..9cfe72b --- /dev/null +++ b/frontend/src/lib/api/hooks/useProjects.ts @@ -0,0 +1,271 @@ +/** + * Projects List Hook + * + * Provides data for the projects list page with filtering, + * sorting, and pagination. + * + * Uses mock data until backend endpoints are available. + * + * @see Issue #54 + */ + +import { useQuery } from '@tanstack/react-query'; +import type { ProjectStatus } from '@/components/projects/types'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ProjectListItem { + id: string; + name: 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[]; +} + +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; + }; +} + +// ============================================================================ +// Mock Data +// ============================================================================ + +const mockProjects: ProjectListItem[] = [ + { + id: 'proj-001', + name: 'E-Commerce Platform Redesign', + description: 'Complete redesign of the e-commerce platform with modern UI/UX and improved checkout flow', + status: 'active', + complexity: 'high', + progress: 67, + openIssues: 12, + activeAgents: 4, + currentSprint: 'Sprint 3', + lastActivity: '2 minutes ago', + createdAt: '2025-11-15T10:00:00Z', + owner: { id: 'user-001', name: 'Felipe Cardoso' }, + tags: ['e-commerce', 'frontend', 'ux'], + }, + { + id: 'proj-002', + name: 'Mobile Banking App', + description: 'Native mobile app for banking services with biometric authentication and real-time notifications', + status: 'active', + complexity: 'high', + progress: 45, + openIssues: 8, + activeAgents: 5, + currentSprint: 'Sprint 2', + lastActivity: '15 minutes ago', + createdAt: '2025-11-20T09:00:00Z', + owner: { id: 'user-001', name: 'Felipe Cardoso' }, + tags: ['mobile', 'fintech', 'security'], + }, + { + id: 'proj-003', + name: 'Internal HR Portal', + description: 'Employee self-service portal for HR operations including leave requests and performance reviews', + status: 'paused', + complexity: 'medium', + progress: 23, + openIssues: 5, + activeAgents: 0, + currentSprint: 'Sprint 1', + lastActivity: '2 days ago', + createdAt: '2025-10-01T08:00:00Z', + owner: { id: 'user-002', name: 'Maria Santos' }, + tags: ['internal', 'hr', 'portal'], + }, + { + id: 'proj-004', + name: 'API Gateway Modernization', + description: 'Migrate legacy API gateway to cloud-native architecture with improved rate limiting and caching', + status: 'active', + complexity: 'high', + progress: 82, + openIssues: 3, + activeAgents: 2, + currentSprint: 'Sprint 4', + lastActivity: '1 hour ago', + createdAt: '2025-12-01T11:00:00Z', + owner: { id: 'user-001', name: 'Felipe Cardoso' }, + tags: ['api', 'cloud', 'infrastructure'], + }, + { + id: 'proj-005', + name: 'Customer Analytics Dashboard', + description: 'Real-time analytics dashboard for customer behavior insights with ML-powered predictions', + status: 'completed', + complexity: 'medium', + progress: 100, + openIssues: 0, + activeAgents: 0, + lastActivity: '2 weeks ago', + createdAt: '2025-09-01T10:00:00Z', + owner: { id: 'user-003', name: 'Alex Johnson' }, + tags: ['analytics', 'ml', 'dashboard'], + }, + { + id: 'proj-006', + name: 'DevOps Pipeline Automation', + description: 'Automate CI/CD pipelines with AI-assisted deployments and rollback capabilities', + status: 'active', + complexity: 'medium', + progress: 35, + openIssues: 6, + activeAgents: 3, + currentSprint: 'Sprint 1', + lastActivity: '30 minutes ago', + createdAt: '2025-12-10T14:00:00Z', + owner: { id: 'user-001', name: 'Felipe Cardoso' }, + tags: ['devops', 'automation', 'ci-cd'], + }, + { + id: 'proj-007', + name: 'Inventory Management System', + description: 'Warehouse inventory tracking with barcode scanning and automated reordering', + status: 'archived', + complexity: 'low', + progress: 100, + openIssues: 0, + activeAgents: 0, + lastActivity: '1 month ago', + createdAt: '2025-06-15T08:00:00Z', + owner: { id: 'user-002', name: 'Maria Santos' }, + tags: ['inventory', 'warehouse', 'logistics'], + }, + { + id: 'proj-008', + name: 'Customer Support Chatbot', + description: 'AI-powered chatbot for 24/7 customer support with sentiment analysis', + status: 'active', + complexity: 'medium', + progress: 58, + openIssues: 4, + activeAgents: 2, + currentSprint: 'Sprint 2', + lastActivity: '45 minutes ago', + createdAt: '2025-12-05T09:00:00Z', + owner: { id: 'user-003', name: 'Alex Johnson' }, + tags: ['ai', 'chatbot', 'support'], + }, +]; + +// ============================================================================ +// 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({ + queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }], + queryFn: async () => { + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 400)); + + // Filter projects + let filtered = [...mockProjects]; + + // Search filter + if (search) { + const searchLower = search.toLowerCase(); + filtered = filtered.filter( + (p) => + p.name.toLowerCase().includes(searchLower) || + p.description?.toLowerCase().includes(searchLower) || + p.tags?.some((t) => t.toLowerCase().includes(searchLower)) + ); + } + + // Status filter + if (status !== 'all') { + filtered = filtered.filter((p) => p.status === status); + } + + // Complexity filter + if (complexity !== 'all') { + filtered = filtered.filter((p) => p.complexity === complexity); + } + + // Sort + filtered.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; + }); + + // Pagination + const total = filtered.length; + const totalPages = Math.ceil(total / limit); + const start = (page - 1) * limit; + const end = start + limit; + const paginatedData = filtered.slice(start, end); + + return { + data: paginatedData, + pagination: { + page, + limit, + total, + totalPages, + }, + }; + }, + staleTime: 30000, + }); +}