Spaces:
Sleeping
Sleeping
| 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; | |