A project tracker for decentralized social media platforms, clients, and tools

feat: enhance project cards and UI improvements

- Add warning indicator for semi-platform projects with hover tooltip
- Remove automatic network and type badges, show only project tags
- Display full descriptions and all tags without truncation
- Update search placeholder to indicate tag search capability
- Add bottom padding for better scroll experience
- Update Bluesky project tags to include ios/android platforms

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+22 -21
+1 -1
public/data/projects.json
··· 8 "logoUrl": "/logos/bluesky.svg", 9 "description": "A new kind of social network that puts users in control of their experience and gives creators independence from platforms.", 10 "type": "platform", 11 - "tags": ["microblog", "flagship", "open-source", "mobile", "web"], 12 "languages": ["TypeScript", "React Native"], 13 "links": [ 14 { "kind": "homepage", "url": "https://bsky.app" },
··· 8 "logoUrl": "/logos/bluesky.svg", 9 "description": "A new kind of social network that puts users in control of their experience and gives creators independence from platforms.", 10 "type": "platform", 11 + "tags": ["microblog", "flagship", "open-source", "mobile", "web", "ios", "android"], 12 "languages": ["TypeScript", "React Native"], 13 "links": [ 14 { "kind": "homepage", "url": "https://bsky.app" },
+2 -2
src/App.tsx
··· 75 selectedNetwork={filters.network} 76 onNetworkChange={handleNetworkChange} 77 /> 78 - <div className="container mx-auto px-4 pt-20"> 79 <FilterToolbar 80 query={filters.query} 81 selectedTags={filters.tags} ··· 86 onTagsChange={handleTagsChange} 87 onSortChange={handleSortChange} 88 /> 89 - <ProjectGrid 90 projects={filteredProjects} 91 loading={loading} 92 />
··· 75 selectedNetwork={filters.network} 76 onNetworkChange={handleNetworkChange} 77 /> 78 + <div className="container mx-auto px-4 pt-20 pb-20"> 79 <FilterToolbar 80 query={filters.query} 81 selectedTags={filters.tags} ··· 86 onTagsChange={handleTagsChange} 87 onSortChange={handleSortChange} 88 /> 89 + <ProjectGrid 90 projects={filteredProjects} 91 loading={loading} 92 />
+1 -1
src/components/FilterToolbar.tsx
··· 79 type="text" 80 value={query} 81 onChange={(e) => onSearchChange(e.target.value)} 82 - placeholder="Search projects" 83 className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-500 focus:outline-none focus:border-blue-500" 84 /> 85 <svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
··· 79 type="text" 80 value={query} 81 onChange={(e) => onSearchChange(e.target.value)} 82 + placeholder="Search projects or tags" 83 className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 placeholder-gray-500 focus:outline-none focus:border-blue-500" 84 /> 85 <svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+18 -17
src/components/ProjectCard.tsx
··· 84 <div className="flex items-start justify-between"> 85 <div className="flex items-center space-x-3"> 86 {project.logoUrl ? ( 87 - <img 88 - src={project.logoUrl} 89 alt={`${project.name} logo`} 90 className="w-10 h-10 rounded-lg object-cover" 91 /> ··· 106 )} 107 </div> 108 </div> 109 </div> 110 111 {project.bannerUrl && ( ··· 118 </div> 119 )} 120 121 - <p className="text-gray-300 text-sm line-clamp-2"> 122 {project.description} 123 </p> 124 125 <div className="flex flex-wrap gap-2"> 126 - <span className={`px-2 py-1 rounded-full text-xs font-medium ${networkColors[project.network]}`}> 127 - {project.network} 128 - </span> 129 - {project.type && ( 130 - <span className="px-2 py-1 bg-gray-700 text-gray-300 rounded-full text-xs"> 131 - {project.type} 132 - </span> 133 - )} 134 - {project.tags.slice(0, 3).map(tag => ( 135 <span key={tag} className="px-2 py-1 bg-gray-700 text-gray-300 rounded-full text-xs"> 136 {tag} 137 </span> 138 ))} 139 - {project.tags.length > 3 && ( 140 - <span className="px-2 py-1 text-gray-500 text-xs"> 141 - +{project.tags.length - 3} 142 - </span> 143 - )} 144 </div> 145 146 <div className="flex-grow"></div>
··· 84 <div className="flex items-start justify-between"> 85 <div className="flex items-center space-x-3"> 86 {project.logoUrl ? ( 87 + <img 88 + src={project.logoUrl} 89 alt={`${project.name} logo`} 90 className="w-10 h-10 rounded-lg object-cover" 91 /> ··· 106 )} 107 </div> 108 </div> 109 + {project.type === 'semi-platform' && ( 110 + <div className="relative group"> 111 + <svg 112 + className="w-5 h-5 text-yellow-500" 113 + fill="currentColor" 114 + viewBox="0 0 20 20" 115 + > 116 + <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> 117 + </svg> 118 + <div className="absolute right-0 top-6 w-48 p-2 bg-gray-900 text-gray-200 text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10 border border-gray-700"> 119 + Has not implemented platform-based AT Protocol lexicon 120 + </div> 121 + </div> 122 + )} 123 </div> 124 125 {project.bannerUrl && ( ··· 132 </div> 133 )} 134 135 + <p className="text-gray-300 text-sm"> 136 {project.description} 137 </p> 138 139 <div className="flex flex-wrap gap-2"> 140 + {project.tags.map(tag => ( 141 <span key={tag} className="px-2 py-1 bg-gray-700 text-gray-300 rounded-full text-xs"> 142 {tag} 143 </span> 144 ))} 145 </div> 146 147 <div className="flex-grow"></div>