|
1 | 1 | import React, { useState } from 'react'; |
2 | | -import { Link } from 'react-router-dom'; |
3 | | - |
| 2 | +import { Link, useNavigate } from 'react-router-dom'; |
| 3 | +import { MagnifyingGlass, Funnel, CaretUp, CaretDown, Check } from '@phosphor-icons/react'; |
4 | 4 | import { getStatusClasses, getPriorityClasses, statusTextColor } from '../../utils/roadmapHelpers'; |
5 | 5 |
|
6 | 6 | const TableView = ({ issuesData = [] }) => { |
| 7 | + const navigate = useNavigate(); |
7 | 8 | const [sortBy, setSortBy] = useState('title'); |
8 | 9 | const [sortOrder, setSortOrder] = useState('asc'); // 'asc' or 'desc' |
9 | | - const [activeFilters, setActiveFilters] = useState(['Planned', 'In Progress', 'On Hold', 'Completed']); // Changed filterStatus to activeFilters |
| 10 | + const [activeFilters, setActiveFilters] = useState(['Planned', 'In Progress', 'On Hold', 'Completed']); |
| 11 | + const [searchQuery, setSearchQuery] = useState(''); |
10 | 12 |
|
11 | 13 | const handleFilterChange = (status) => { |
12 | 14 | if (activeFilters.includes(status)) { |
13 | | - setActiveFilters(activeFilters.filter((s) => s !== status)); // Remove filter |
| 15 | + setActiveFilters(activeFilters.filter((s) => s !== status)); |
14 | 16 | } else { |
15 | | - setActiveFilters([...activeFilters, status]); // Add filter |
| 17 | + setActiveFilters([...activeFilters, status]); |
16 | 18 | } |
17 | 19 | }; |
18 | 20 |
|
19 | 21 | const filteredApps = issuesData.filter((app) => { |
20 | | - if (activeFilters.length === 0) return false; // Show none if no filters active |
21 | | - return activeFilters.includes(app.status || 'Planned'); |
| 22 | + const matchesFilter = activeFilters.length === 0 || activeFilters.includes(app.status || 'Planned'); |
| 23 | + const matchesSearch = (app.title?.toLowerCase() || '').includes(searchQuery.toLowerCase()) || |
| 24 | + (app.description?.toLowerCase() || '').includes(searchQuery.toLowerCase()); |
| 25 | + return matchesFilter && matchesSearch; |
22 | 26 | }); |
23 | 27 |
|
24 | 28 | const sortedApps = [...filteredApps].sort((a, b) => { |
@@ -51,110 +55,160 @@ const TableView = ({ issuesData = [] }) => { |
51 | 55 | } |
52 | 56 | }; |
53 | 57 |
|
54 | | - const renderSortArrow = (column) => { |
55 | | - if (sortBy === column) { |
56 | | - return sortOrder === 'asc' ? ' ↑' : ' ↓'; |
57 | | - } |
58 | | - return ''; |
| 58 | + const SortIcon = ({ column }) => { |
| 59 | + if (sortBy !== column) return <div className="w-4 h-4" />; // Placeholder to prevent layout shift |
| 60 | + return sortOrder === 'asc' ? <CaretUp weight="bold" size={14} /> : <CaretDown weight="bold" size={14} />; |
59 | 61 | }; |
60 | 62 |
|
61 | 63 | return ( |
62 | | - <div className="overflow-x-auto rounded-xl shadow-lg bg-gray-900/70 backdrop-blur-sm border border-gray-800"> |
63 | | - <div className="mb-4 mt-4 flex justify-center items-center flex-wrap gap-2"> |
64 | | - {['Planned', 'In Progress', 'Completed', 'On Hold'].map((status) => ( |
65 | | - <button |
66 | | - key={status} |
67 | | - onClick={() => handleFilterChange(status)} |
68 | | - className={`px-4 py-2 rounded-md text-sm font-mono transition-colors border ${ |
69 | | - activeFilters.includes(status) |
70 | | - ? `${getStatusClasses(status).split(' ')[0]} ${getStatusClasses(status).split(' ')[1]} ${statusTextColor(status)}` |
71 | | - : 'bg-gray-800/60 border-gray-700 text-gray-300 hover:border-indigo-500 hover:text-white' |
72 | | - }`} |
73 | | - > |
74 | | - {status} |
75 | | - </button> |
76 | | - ))} |
77 | | - </div> |
78 | | - <table className="min-w-full divide-y divide-gray-700 text-white"> |
79 | | - <thead className="bg-gray-800/60 border-b border-gray-700"> |
80 | | - <tr> |
81 | | - <th |
82 | | - scope="col" |
83 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide cursor-pointer hover:text-white transition-colors" |
84 | | - onClick={() => handleSort('title')} |
85 | | - > |
86 | | - Title {renderSortArrow('title')} |
87 | | - </th> |
88 | | - <th |
89 | | - scope="col" |
90 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide" |
91 | | - > |
92 | | - Description |
93 | | - </th> |
94 | | - <th |
95 | | - scope="col" |
96 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide cursor-pointer hover:text-white transition-colors" |
97 | | - onClick={() => handleSort('status')} |
98 | | - > |
99 | | - Status {renderSortArrow('status')} |
100 | | - </th> |
101 | | - <th |
102 | | - scope="col" |
103 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide cursor-pointer hover:text-white transition-colors" |
104 | | - onClick={() => handleSort('priority')} |
105 | | - > |
106 | | - Priority {renderSortArrow('priority')} |
107 | | - </th> |
108 | | - <th |
109 | | - scope="col" |
110 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide cursor-pointer hover:text-white transition-colors" |
111 | | - onClick={() => handleSort('created_at')} |
112 | | - > |
113 | | - Created At {renderSortArrow('created_at')} |
114 | | - </th> |
115 | | - <th |
116 | | - scope="col" |
117 | | - className="px-6 py-3 text-left text-sm font-mono font-bold text-gray-400 uppercase tracking-wide" |
118 | | - > |
119 | | - Notes |
120 | | - </th> |
121 | | - </tr> |
122 | | - </thead> |
123 | | - <tbody className="divide-y divide-gray-700"> |
124 | | - {sortedApps.map((app, index) => ( |
125 | | - <tr key={app.id} className={`group hover:bg-indigo-500/20 transition-colors ${index % 2 === 0 ? 'bg-gray-900/40' : 'bg-gray-800/40'}`}> |
126 | | - <td className="px-6 py-4 whitespace-nowrap text-sm font-mono font-medium text-white"> |
127 | | - <Link to={`/roadmap/${app.id}`} className="hover:underline text-purple-400"> |
128 | | - {app.title} |
129 | | - </Link> |
130 | | - </td> |
131 | | - <td className="px-6 py-4 text-sm font-mono text-gray-400"> |
132 | | - {app.description} |
133 | | - </td> |
134 | | - <td className="px-6 py-4 whitespace-nowrap"> |
135 | | - <span |
136 | | - className={`px-2 py-0 inline-flex text-xs font-mono font-semibold rounded-md shadow-sm border ${getStatusClasses(app.status)} ${statusTextColor(app.status)} `} |
137 | | - > |
138 | | - {app.status || 'Planned'} |
139 | | - </span> |
140 | | - </td> |
141 | | - <td className="px-6 py-4 whitespace-nowrap"> |
142 | | - <span |
143 | | - className={`px-2 py-0 inline-flex text-xs font-mono font-semibold rounded-md shadow-sm border ${getPriorityClasses(app.priority)}`} |
144 | | - > |
145 | | - {app.priority || 'Low'} |
| 64 | + <div className="space-y-6"> |
| 65 | + {/* Toolbar: Search and Filters */} |
| 66 | + <div className="bg-gray-900/70 backdrop-blur-md rounded-2xl border border-gray-800 shadow-xl p-4 md:p-5 flex flex-col lg:flex-row gap-6 justify-between items-center"> |
| 67 | + {/* Search */} |
| 68 | + <div className="relative w-full lg:max-w-md group"> |
| 69 | + <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> |
| 70 | + <MagnifyingGlass className="text-gray-500 group-focus-within:text-primary-400 transition-colors" size={20} /> |
| 71 | + </div> |
| 72 | + <input |
| 73 | + type="text" |
| 74 | + placeholder="Search issues by title or description..." |
| 75 | + value={searchQuery} |
| 76 | + onChange={(e) => setSearchQuery(e.target.value)} |
| 77 | + className="block w-full pl-11 pr-4 py-3 border border-gray-700 rounded-xl leading-5 bg-gray-800/50 text-gray-300 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 focus:bg-gray-800 transition-all duration-300 text-sm font-mono" |
| 78 | + /> |
| 79 | + </div> |
| 80 | + |
| 81 | + {/* Filters */} |
| 82 | + <div className="flex flex-col sm:flex-row items-center gap-3 w-full lg:w-auto justify-center lg:justify-end"> |
| 83 | + <div className="flex items-center text-gray-400 font-mono text-xs uppercase tracking-wider"> |
| 84 | + <Funnel size={16} className="mr-2" /> Filter Status: |
| 85 | + </div> |
| 86 | + <div className="flex flex-wrap justify-center gap-2"> |
| 87 | + {['Planned', 'In Progress', 'Completed', 'On Hold'].map((status) => ( |
| 88 | + <button |
| 89 | + key={status} |
| 90 | + onClick={() => handleFilterChange(status)} |
| 91 | + className={` |
| 92 | + group relative px-3 py-1.5 rounded-lg text-xs font-mono font-bold transition-all duration-200 border select-none |
| 93 | + ${activeFilters.includes(status) |
| 94 | + ? `${getStatusClasses(status).split(' ')[0]} ${getStatusClasses(status).split(' ')[1]} ${statusTextColor(status)} shadow-md ring-1 ring-white/10` |
| 95 | + : 'bg-gray-800/40 border-gray-700 text-gray-500 hover:border-gray-600 hover:bg-gray-800 hover:text-gray-300' |
| 96 | + } |
| 97 | + `} |
| 98 | + > |
| 99 | + <span className="flex items-center gap-1.5"> |
| 100 | + {activeFilters.includes(status) && <Check weight="bold" />} |
| 101 | + {status} |
146 | 102 | </span> |
147 | | - </td> |
148 | | - <td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-400"> |
149 | | - {new Date(app.created_at).toLocaleDateString()} |
150 | | - </td> |
151 | | - <td className="px-6 py-4 text-sm font-mono text-gray-500"> |
152 | | - {app.notes || '-'} |
153 | | - </td> |
154 | | - </tr> |
155 | | - ))} |
156 | | - </tbody> |
157 | | - </table> |
| 103 | + </button> |
| 104 | + ))} |
| 105 | + </div> |
| 106 | + </div> |
| 107 | + </div> |
| 108 | + |
| 109 | + {/* Table */} |
| 110 | + <div className="overflow-hidden rounded-2xl shadow-2xl bg-gray-900/70 backdrop-blur-md border border-gray-800"> |
| 111 | + <div className="overflow-x-auto"> |
| 112 | + <table className="min-w-full divide-y divide-gray-800"> |
| 113 | + <thead> |
| 114 | + <tr className="bg-gray-800/60"> |
| 115 | + {[ |
| 116 | + { key: 'title', label: 'Title' }, |
| 117 | + { key: 'description', label: 'Description', noSort: true }, |
| 118 | + { key: 'status', label: 'Status' }, |
| 119 | + { key: 'priority', label: 'Priority' }, |
| 120 | + { key: 'created_at', label: 'Created' }, |
| 121 | + { key: 'notes', label: 'Notes', noSort: true }, |
| 122 | + ].map((col) => ( |
| 123 | + <th |
| 124 | + key={col.key} |
| 125 | + scope="col" |
| 126 | + onClick={() => !col.noSort && handleSort(col.key)} |
| 127 | + className={` |
| 128 | + px-6 py-4 text-left text-xs font-mono font-bold text-gray-400 uppercase tracking-wider |
| 129 | + ${!col.noSort ? 'cursor-pointer hover:text-primary-400 hover:bg-gray-800/50 transition-colors select-none' : ''} |
| 130 | + `} |
| 131 | + > |
| 132 | + <div className="flex items-center gap-2"> |
| 133 | + {col.label} |
| 134 | + {!col.noSort && <SortIcon column={col.key} />} |
| 135 | + </div> |
| 136 | + </th> |
| 137 | + ))} |
| 138 | + </tr> |
| 139 | + </thead> |
| 140 | + <tbody className="divide-y divide-gray-800/50"> |
| 141 | + {sortedApps.length > 0 ? ( |
| 142 | + sortedApps.map((app, index) => ( |
| 143 | + <tr |
| 144 | + key={app.id} |
| 145 | + onClick={(e) => { |
| 146 | + // Navigate if the click didn't originate from a link |
| 147 | + if (!e.target.closest('a')) { |
| 148 | + navigate(`/roadmap/${app.id}`); |
| 149 | + } |
| 150 | + }} |
| 151 | + className={` |
| 152 | + group transition-colors duration-200 cursor-pointer |
| 153 | + ${index % 2 === 0 ? 'bg-gray-900/20' : 'bg-transparent'} |
| 154 | + hover:!bg-gray-800/60 |
| 155 | + `} |
| 156 | + > |
| 157 | + <td className="px-6 py-4 whitespace-nowrap"> |
| 158 | + <Link to={`/roadmap/${app.id}`} className="text-sm font-mono font-bold text-white group-hover:text-primary-400 transition-colors"> |
| 159 | + {app.title} |
| 160 | + </Link> |
| 161 | + </td> |
| 162 | + <td className="px-6 py-4 text-sm text-gray-400 font-mono max-w-xs truncate" title={app.description}> |
| 163 | + {app.description} |
| 164 | + </td> |
| 165 | + <td className="px-6 py-4 whitespace-nowrap"> |
| 166 | + <span className={`px-2.5 py-1 inline-flex items-center text-[10px] font-mono font-bold uppercase tracking-wide rounded-full border ${getStatusClasses(app.status)} ${statusTextColor(app.status)} shadow-sm`}> |
| 167 | + {app.status || 'Planned'} |
| 168 | + </span> |
| 169 | + </td> |
| 170 | + <td className="px-6 py-4 whitespace-nowrap"> |
| 171 | + <span className={`flex items-center gap-2 text-xs font-mono font-bold ${getPriorityClasses(app.priority).split(' ')[0]}`}> |
| 172 | + <span className={`w-2 h-2 rounded-full ${ |
| 173 | + app.priority === 'High' ? 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]' : |
| 174 | + app.priority === 'Medium' ? 'bg-yellow-500 shadow-[0_0_8px_rgba(234,179,8,0.6)]' : |
| 175 | + 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]' |
| 176 | + }`}></span> |
| 177 | + {app.priority || 'Low'} |
| 178 | + </span> |
| 179 | + </td> |
| 180 | + <td className="px-6 py-4 whitespace-nowrap text-xs font-mono text-gray-500"> |
| 181 | + {new Date(app.created_at).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} |
| 182 | + </td> |
| 183 | + <td className="px-6 py-4 text-xs text-gray-500 font-mono italic max-w-xs truncate"> |
| 184 | + {app.notes || '-'} |
| 185 | + </td> |
| 186 | + </tr> |
| 187 | + )) |
| 188 | + ) : ( |
| 189 | + <tr> |
| 190 | + <td colSpan="6" className="px-6 py-16 text-center text-gray-500 font-mono"> |
| 191 | + <div className="flex flex-col items-center justify-center gap-4"> |
| 192 | + <div className="p-4 rounded-full bg-gray-800/50"> |
| 193 | + <MagnifyingGlass size={32} className="opacity-50" /> |
| 194 | + </div> |
| 195 | + <p className="text-lg font-medium text-gray-400">No issues found</p> |
| 196 | + <p className="text-sm">Try adjusting your search or filters.</p> |
| 197 | + </div> |
| 198 | + </td> |
| 199 | + </tr> |
| 200 | + )} |
| 201 | + </tbody> |
| 202 | + </table> |
| 203 | + </div> |
| 204 | + </div> |
| 205 | + |
| 206 | + {/* Footer info */} |
| 207 | + <div className="flex justify-end items-center px-2"> |
| 208 | + <span className="text-xs font-mono text-gray-600 bg-gray-900/50 px-3 py-1 rounded-full border border-gray-800"> |
| 209 | + Showing <span className="text-primary-400 font-bold">{sortedApps.length}</span> of {issuesData.length} issues |
| 210 | + </span> |
| 211 | + </div> |
158 | 212 | </div> |
159 | 213 | ); |
160 | 214 | }; |
|
0 commit comments