forked from cardosofelipe/fast-next-template
Implement the main dashboard / projects list page for Syndarix as the landing page after login. The implementation includes: Dashboard Components: - QuickStats: Overview cards showing active projects, agents, issues, approvals - ProjectsSection: Grid/list view with filtering and sorting controls - ProjectCardGrid: Rich project cards for grid view - ProjectRowList: Compact rows for list view - ActivityFeed: Real-time activity sidebar with connection status - PerformanceCard: Performance metrics display - EmptyState: Call-to-action for new users - ProjectStatusBadge: Status indicator with icons - ComplexityIndicator: Visual complexity dots - ProgressBar: Accessible progress bar component Features: - Projects grid/list view with view mode toggle - Filter by status (all, active, paused, completed, archived) - Sort by recent, name, progress, or issues - Quick stats overview with counts - Real-time activity feed sidebar with live/reconnecting status - Performance metrics card - Create project button linking to wizard - Responsive layout for mobile/desktop - Loading skeleton states - Empty state for new users API Integration: - useProjects hook for fetching projects (mock data until backend ready) - useDashboardStats hook for statistics - TanStack Query for caching and data fetching Testing: - 37 unit tests covering all dashboard components - E2E test suite for dashboard functionality - Accessibility tests (keyboard nav, aria attributes, heading hierarchy) Technical: - TypeScript strict mode compliance - ESLint passing - WCAG AA accessibility compliance - Mobile-first responsive design - Dark mode support via semantic tokens - Follows design system guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
202 lines
5.7 KiB
TypeScript
202 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Project Issues List Page
|
|
*
|
|
* Displays filterable, sortable list of issues for a project.
|
|
* Supports bulk actions and sync with external trackers.
|
|
*
|
|
* @module app/[locale]/(authenticated)/projects/[id]/issues/page
|
|
*/
|
|
|
|
import { useState, use } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Plus, Upload } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
IssueFilters,
|
|
IssueTable,
|
|
BulkActions,
|
|
useIssues,
|
|
} from '@/features/issues';
|
|
import type { IssueFiltersType, IssueSort } from '@/features/issues';
|
|
|
|
interface ProjectIssuesPageProps {
|
|
params: Promise<{
|
|
locale: string;
|
|
id: string;
|
|
}>;
|
|
}
|
|
|
|
export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
|
|
const { locale, id: projectId } = use(params);
|
|
const router = useRouter();
|
|
|
|
// Filter state
|
|
const [filters, setFilters] = useState<IssueFiltersType>({
|
|
status: 'all',
|
|
priority: 'all',
|
|
sprint: 'all',
|
|
assignee: 'all',
|
|
});
|
|
|
|
// Sort state
|
|
const [sort, setSort] = useState<IssueSort>({
|
|
field: 'updated_at',
|
|
direction: 'desc',
|
|
});
|
|
|
|
// Selection state
|
|
const [selectedIssues, setSelectedIssues] = useState<string[]>([]);
|
|
|
|
// Fetch issues
|
|
const { data, isLoading, error } = useIssues(projectId, filters, sort);
|
|
|
|
const handleIssueClick = (issueId: string) => {
|
|
router.push(`/${locale}/projects/${projectId}/issues/${issueId}`);
|
|
};
|
|
|
|
const handleBulkChangeStatus = () => {
|
|
// TODO: Open status change dialog
|
|
console.log('Change status for:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkAssign = () => {
|
|
// TODO: Open assign dialog
|
|
console.log('Assign:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkAddLabels = () => {
|
|
// TODO: Open labels dialog
|
|
console.log('Add labels to:', selectedIssues);
|
|
};
|
|
|
|
const handleBulkDelete = () => {
|
|
// TODO: Confirm and delete
|
|
console.log('Delete:', selectedIssues);
|
|
};
|
|
|
|
const handleSync = () => {
|
|
// TODO: Sync all issues
|
|
console.log('Sync issues');
|
|
};
|
|
|
|
const handleNewIssue = () => {
|
|
// TODO: Navigate to new issue page or open dialog
|
|
console.log('Create new issue');
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="rounded-lg border border-destructive bg-destructive/10 p-6 text-center">
|
|
<h2 className="text-lg font-semibold text-destructive">Error Loading Issues</h2>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Failed to load issues. Please try again later.
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
className="mt-4"
|
|
onClick={() => window.location.reload()}
|
|
>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6">
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Issues</h1>
|
|
<p className="text-muted-foreground">
|
|
{isLoading ? (
|
|
<Skeleton className="h-4 w-24" />
|
|
) : (
|
|
`${data?.pagination.total || 0} issues found`
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={handleSync}>
|
|
<Upload className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
Sync
|
|
</Button>
|
|
<Button size="sm" onClick={handleNewIssue}>
|
|
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
New Issue
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<IssueFilters filters={filters} onFiltersChange={setFilters} />
|
|
|
|
{/* Bulk Actions */}
|
|
<BulkActions
|
|
selectedCount={selectedIssues.length}
|
|
onChangeStatus={handleBulkChangeStatus}
|
|
onAssign={handleBulkAssign}
|
|
onAddLabels={handleBulkAddLabels}
|
|
onDelete={handleBulkDelete}
|
|
/>
|
|
|
|
{/* Issue Table */}
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{[...Array(5)].map((_, i) => (
|
|
<Skeleton key={i} className="h-16 w-full" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<IssueTable
|
|
issues={data?.data || []}
|
|
selectedIssues={selectedIssues}
|
|
onSelectionChange={setSelectedIssues}
|
|
onIssueClick={handleIssueClick}
|
|
sort={sort}
|
|
onSortChange={setSort}
|
|
/>
|
|
)}
|
|
|
|
{/* Pagination info */}
|
|
{data && data.pagination.total > 0 && (
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>
|
|
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
|
|
{Math.min(
|
|
data.pagination.page * data.pagination.page_size,
|
|
data.pagination.total
|
|
)}{' '}
|
|
of {data.pagination.total} issues
|
|
</span>
|
|
{data.pagination.total_pages > 1 && (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={!data.pagination.has_prev}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={!data.pagination.has_next}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|