at main 11 kB view raw
1'use client' 2 3import { useState } from 'react' 4import Link from 'next/link' 5import { ArrowUpDown, ExternalLink, Search, X } from 'lucide-react' 6import { PieChart, Pie, ResponsiveContainer } from 'recharts' 7import { Project, SortConfig } from '@/lib/types' 8import { formatCurrency } from '@/lib/data' 9 10interface ProjectTableProps { 11 projects: Project[] 12} 13 14export default function ProjectTable({ projects }: ProjectTableProps) { 15 const [sortConfig, setSortConfig] = useState<SortConfig>({ 16 key: 'fundingReceived', 17 direction: 'desc' 18 }) 19 const [searchQuery, setSearchQuery] = useState('') 20 21 // Filter projects based on search query 22 const filteredProjects = projects.filter(project => { 23 if (!searchQuery) return true 24 25 const query = searchQuery.toLowerCase() 26 return ( 27 project.name.toLowerCase().includes(query) || 28 project.description.toLowerCase().includes(query) || 29 project.categories.some(category => category.toLowerCase().includes(query)) 30 ) 31 }) 32 33 // Sort filtered projects 34 const sortedProjects = [...filteredProjects].sort((a, b) => { 35 const aValue = a[sortConfig.key] 36 const bValue = b[sortConfig.key] 37 38 if (typeof aValue === 'number' && typeof bValue === 'number') { 39 return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue 40 } 41 42 const aString = String(aValue).toLowerCase() 43 const bString = String(bValue).toLowerCase() 44 45 if (sortConfig.direction === 'asc') { 46 return aString.localeCompare(bString) 47 } else { 48 return bString.localeCompare(aString) 49 } 50 }) 51 52 const handleSort = (key: keyof Project) => { 53 setSortConfig({ 54 key, 55 direction: sortConfig.key === key && sortConfig.direction === 'desc' ? 'asc' : 'desc' 56 }) 57 } 58 59 60 const SortButton = ({ column, label }: { column: keyof Project; label: string }) => ( 61 <button 62 onClick={() => handleSort(column)} 63 className="group flex items-center space-x-1 text-xs font-medium text-secondary uppercase tracking-wider hover:text-primary transition-colors" 64 > 65 <span>{label}</span> 66 <ArrowUpDown className="h-3 w-3 opacity-60 group-hover:opacity-100" /> 67 </button> 68 ) 69 70 return ( 71 <div className="space-y-4"> 72 {/* Search Bar */} 73 <div className="relative w-full sm:max-w-sm mb-4 sm:mb-0"> 74 <input 75 type="text" 76 placeholder="Search projects..." 77 value={searchQuery} 78 onChange={(e) => setSearchQuery(e.target.value)} 79 className="w-full px-4 py-2 bg-transparent border-0 border-b border-border text-primary placeholder-muted focus:outline-none focus:border-accent transition-elegant" 80 /> 81 {searchQuery && ( 82 <button 83 onClick={() => setSearchQuery('')} 84 className="absolute right-1 top-1/2 transform -translate-y-1/2 p-1 text-muted hover:text-accent transition-colors" 85 > 86 <X className="h-3 w-3" /> 87 </button> 88 )} 89 </div> 90 91 {/* Results count */} 92 {searchQuery && ( 93 <div className="text-sm text-secondary mb-4"> 94 {sortedProjects.length} of {projects.length} projects found 95 </div> 96 )} 97 98 <div className="overflow-x-auto"> 99 <table className="w-full min-w-[800px]"> 100 <thead> 101 <tr className="bg-accent-light border-b border-border"> 102 <th className="text-left py-3 px-2 sm:px-4"> 103 <SortButton column="name" label="Project" /> 104 </th> 105 <th className="text-left py-3 px-2 sm:px-4 hidden sm:table-cell"> 106 <SortButton column="fundingReceived" label="Funding" /> 107 </th> 108 <th className="text-left py-3 px-2 sm:px-4 hidden md:table-cell"> 109 <SortButton column="sustainableRevenuePercent" label="Sustainability" /> 110 </th> 111 <th className="text-left py-3 px-2 sm:px-4 hidden lg:table-cell"> 112 <SortButton column="annualBudget" label="Budget" /> 113 </th> 114 <th className="text-left py-3 px-2 sm:px-4 hidden lg:table-cell"> 115 <SortButton column="teamSize" label="Team" /> 116 </th> 117 <th className="text-left py-3 px-2 sm:px-4 hidden md:table-cell"> 118 <SortButton column="fundingGivenOut" label="Distributed" /> 119 </th> 120 <th className="text-left py-3 px-2 sm:px-4 hidden sm:table-cell"> 121 <span className="text-xs font-medium text-secondary uppercase tracking-wider">Categories</span> 122 </th> 123 </tr> 124 </thead> 125 <tbody> 126 {sortedProjects.map((project) => ( 127 <tr 128 key={project.id} 129 className="hover:bg-surface-hover transition-colors group border-b border-border/30" 130 > 131 <td className="py-2 px-2 sm:px-4"> 132 <div className="flex items-center space-x-3 sm:space-x-4"> 133 <div className="flex-shrink-0"> 134 <div className="w-8 h-8 bg-gray-100 dark:bg-gray-800 flex items-center justify-center"> 135 <span className="text-xs font-semibold text-gray-700 dark:text-gray-300"> 136 {project.name.substring(0, 2).toUpperCase()} 137 </span> 138 </div> 139 </div> 140 <div className="min-w-0 flex-1"> 141 <div className="flex items-center space-x-2"> 142 <Link 143 href={`/project/${project.id}`} 144 className="text-sm font-semibold text-primary hover:text-accent transition-colors" 145 > 146 {project.name} 147 </Link> 148 <a 149 href={project.website} 150 target="_blank" 151 rel="noopener noreferrer" 152 className="opacity-0 group-hover:opacity-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition-all" 153 > 154 <ExternalLink className="h-3 w-3" /> 155 </a> 156 </div> 157 <p className="text-xs text-secondary mt-1 max-w-xs truncate"> 158 {project.description} 159 </p> 160 {/* Mobile-only funding info */} 161 <div className="sm:hidden mt-2 flex flex-wrap gap-2 text-xs"> 162 <span className="font-medium text-primary">{formatCurrency(project.fundingReceived)}</span> 163 <span className="text-muted"></span> 164 <span className="text-secondary">{project.sustainableRevenuePercent}% sustainable</span> 165 </div> 166 </div> 167 </div> 168 </td> 169 <td className="py-2 px-2 sm:px-4 hidden sm:table-cell"> 170 <div className="text-xs font-semibold text-primary"> 171 {formatCurrency(project.fundingReceived)} 172 </div> 173 <div className="text-xs text-muted"> 174 Total raised 175 </div> 176 </td> 177 <td className="py-2 px-2 sm:px-4 hidden md:table-cell"> 178 <div className="flex items-center space-x-3"> 179 <div className="w-8 h-8"> 180 <ResponsiveContainer width="100%" height="100%"> 181 <PieChart> 182 <Pie 183 data={[ 184 { value: project.sustainableRevenuePercent, fill: 'var(--accent)' }, 185 { value: 100 - project.sustainableRevenuePercent, fill: 'var(--border)' } 186 ]} 187 cx="50%" 188 cy="50%" 189 innerRadius={8} 190 outerRadius={16} 191 startAngle={90} 192 endAngle={450} 193 dataKey="value" 194 > 195 </Pie> 196 </PieChart> 197 </ResponsiveContainer> 198 </div> 199 <div> 200 <div className="text-xs font-semibold text-primary"> 201 {project.sustainableRevenuePercent}% 202 </div> 203 <div className="text-xs text-muted"> 204 Sustainable 205 </div> 206 </div> 207 </div> 208 </td> 209 <td className="py-2 px-2 sm:px-4 hidden lg:table-cell"> 210 <div className="text-xs font-semibold text-primary"> 211 {formatCurrency(project.annualBudget)} 212 </div> 213 <div className="text-xs text-muted"> 214 Annual 215 </div> 216 </td> 217 <td className="py-2 px-2 sm:px-4 hidden lg:table-cell"> 218 <div className="text-xs font-semibold text-primary"> 219 {project.teamSize} 220 </div> 221 <div className="text-xs text-muted"> 222 {project.teamSize === 1 ? 'person' : 'people'} 223 </div> 224 </td> 225 <td className="py-2 px-2 sm:px-4 hidden md:table-cell"> 226 <div className="text-xs font-semibold text-primary"> 227 {formatCurrency(project.fundingGivenOut)} 228 </div> 229 <div className="text-xs text-muted"> 230 {((project.fundingGivenOut / project.fundingReceived) * 100).toFixed(0)}% of received 231 </div> 232 </td> 233 <td className="py-2 px-2 sm:px-4 hidden sm:table-cell"> 234 <div className="flex flex-wrap gap-0.5 max-w-32"> 235 {project.categories.slice(0, 2).map((category) => ( 236 <span 237 key={category} 238 className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600" 239 > 240 {category} 241 </span> 242 ))} 243 {project.categories.length > 2 && ( 244 <span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-600"> 245 +{project.categories.length - 2} 246 </span> 247 )} 248 </div> 249 </td> 250 </tr> 251 ))} 252 </tbody> 253 </table> 254 </div> 255 </div> 256 ) 257}