pmtool / src /pages /backlog.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
13.7 kB
import React, { useState } from "react";
import { cn } from "@/lib/utils";
import {
ArrowRight,
CheckCircle,
Circle,
Clock,
Filter,
MoreHorizontal,
Plus,
RefreshCw,
Search,
Triangle
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import Layout from "@/components/layout/layout";
import { getUserById, getProjectById, mockIssues } from "@/lib/data";
import { Issue, IssuePriority, IssueStatus, IssueType } from "@/types";
import { Dropdown, DropdownOption } from "@/components/ui/dropdown";
const Backlog = () => {
const [searchQuery, setSearchQuery] = useState("");
const [projectFilter, setProjectFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState<IssueStatus | "all">("all");
const [typeFilter, setTypeFilter] = useState<IssueType | "all">("all");
const [priorityFilter, setPriorityFilter] = useState<IssuePriority | "all">("all");
const filteredIssues = mockIssues.filter((issue) => {
const matchesSearch =
issue.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
issue.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesProject = projectFilter === "all" || issue.projectId === projectFilter;
const matchesStatus = statusFilter === "all" || issue.status === statusFilter;
const matchesType = typeFilter === "all" || issue.type === typeFilter;
const matchesPriority = priorityFilter === "all" || issue.priority === priorityFilter;
return matchesSearch && matchesProject && matchesStatus && matchesType && matchesPriority;
});
const projects = [...new Set(mockIssues.map(issue => issue.projectId))].map(
projectId => getProjectById(projectId)
).filter(Boolean);
const statusOptions: IssueStatus[] = ["New", "In Progress", "In Review", "Done"];
const typeOptions: IssueType[] = ["Bug", "Feature", "Task", "Improvement"];
const priorityOptions: IssuePriority[] = ["Low", "Medium", "High", "Critical"];
return (
<Layout>
<div className="space-y-6 animate-fadeIn">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Backlog</h1>
<p className="text-muted-foreground">
View and manage all issues and features in your product backlog
</p>
</div>
<div className="flex flex-col space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="relative w-full sm:w-auto max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search backlog..."
className="pl-9 w-full sm:w-80"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button className="w-full sm:w-auto">
<Plus className="mr-2 h-4 w-4" /> Add Issue
</Button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Dropdown
options={[
{ label: "All Projects", value: "all" },
...projects.map(project => ({
label: project.name,
value: project.id
}))
]}
value={projectFilter}
onValueChange={(value) => setProjectFilter(value)}
placeholder="Filter by Project"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Statuses", value: "all" },
...statusOptions.map(status => ({
label: status,
value: status
}))
]}
value={statusFilter}
onValueChange={(value) => setStatusFilter(value as IssueStatus | "all")}
placeholder="Filter by Status"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Types", value: "all" },
...typeOptions.map(type => ({
label: type,
value: type
}))
]}
value={typeFilter}
onValueChange={(value) => setTypeFilter(value as IssueType | "all")}
placeholder="Filter by Type"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Priorities", value: "all" },
...priorityOptions.map(priority => ({
label: priority,
value: priority
}))
]}
value={priorityFilter}
onValueChange={(value) => setPriorityFilter(value as IssuePriority | "all")}
placeholder="Filter by Priority"
className="w-full"
/>
</div>
</div>
{filteredIssues.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="rounded-full bg-muted p-3 mb-4">
<Search className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-1">No issues found</h3>
<p className="text-muted-foreground text-sm mb-4">
Try adjusting your search or filter criteria
</p>
<Button
variant="outline"
onClick={() => {
setSearchQuery("");
setProjectFilter("all");
setStatusFilter("all");
setTypeFilter("all");
setPriorityFilter("all");
}}
>
Reset filters
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-4">
<Card>
<CardHeader className="py-4">
<CardTitle className="text-lg flex justify-between items-center">
<span>Backlog Items ({filteredIssues.length})</span>
<Button variant="ghost" size="sm" className="h-8 gap-1">
<RefreshCw className="h-4 w-4" /> Refresh
</Button>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b text-left">
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Title</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Project</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Type</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Priority</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Status</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Assignee</th>
<th className="py-3 px-4 text-sm font-medium text-muted-foreground">Actions</th>
</tr>
</thead>
<tbody>
{filteredIssues.map((issue) => (
<IssueRow key={issue.id} issue={issue} />
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)}
</div>
</Layout>
);
};
interface IssueRowProps {
issue: Issue;
}
const IssueRow = ({ issue }: IssueRowProps) => {
const project = getProjectById(issue.projectId);
const assignee = issue.assigneeId ? getUserById(issue.assigneeId) : undefined;
// Type icon
const getTypeIcon = (type: IssueType) => {
switch (type) {
case "Bug":
return <Triangle className="h-4 w-4 text-red-500" />;
case "Feature":
return <Plus className="h-4 w-4 text-blue-500" />;
case "Task":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "Improvement":
return <ArrowRight className="h-4 w-4 text-purple-500" />;
default:
return null;
}
};
// Priority badge
const getPriorityBadge = (priority: IssuePriority) => {
switch (priority) {
case "Critical":
return (
<Badge variant="outline" className="bg-red-50 text-red-600 border-red-100">
Critical
</Badge>
);
case "High":
return (
<Badge variant="outline" className="bg-orange-50 text-orange-600 border-orange-100">
High
</Badge>
);
case "Medium":
return (
<Badge variant="outline" className="bg-amber-50 text-amber-600 border-amber-100">
Medium
</Badge>
);
case "Low":
return (
<Badge variant="outline" className="bg-green-50 text-green-600 border-green-100">
Low
</Badge>
);
default:
return null;
}
};
// Status badge
const getStatusBadge = (status: IssueStatus) => {
switch (status) {
case "New":
return (
<div className="flex items-center gap-1">
<Circle className="h-3 w-3 text-blue-500" />
<span>New</span>
</div>
);
case "In Progress":
return (
<div className="flex items-center gap-1">
<RefreshCw className="h-3 w-3 text-orange-500" />
<span>In Progress</span>
</div>
);
case "In Review":
return (
<div className="flex items-center gap-1">
<Clock className="h-3 w-3 text-purple-500" />
<span>In Review</span>
</div>
);
case "Done":
return (
<div className="flex items-center gap-1">
<CheckCircle className="h-3 w-3 text-green-500" />
<span>Done</span>
</div>
);
default:
return null;
}
};
return (
<tr className="border-b hover:bg-muted/30">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
{getTypeIcon(issue.type)}
<span className="font-medium">{issue.title}</span>
</div>
</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="text-sm">{project?.name}</span>
</div>
</td>
<td className="py-3 px-4">
<span className="text-sm">{issue.type}</span>
</td>
<td className="py-3 px-4">
{getPriorityBadge(issue.priority)}
</td>
<td className="py-3 px-4">
<span className="text-sm">{getStatusBadge(issue.status)}</span>
</td>
<td className="py-3 px-4">
{assignee ? (
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarImage src={assignee.avatar} />
<AvatarFallback>
{assignee.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-sm">{assignee.name}</span>
</div>
) : (
<span className="text-sm text-muted-foreground">Unassigned</span>
)}
</td>
<td className="py-3 px-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>View Details</DropdownMenuItem>
<DropdownMenuItem>Edit Issue</DropdownMenuItem>
<DropdownMenuItem>Assign to Me</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-500">
Delete Issue
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
);
};
export default Backlog;