A project tracker for decentralized social media platforms, clients, and tools
at main 2.9 kB view raw
1import { useState, useEffect, useMemo } from 'react'; 2import type { Network, Project, FilterState } from './types/project'; 3import TopBar from './components/TopBar'; 4import FilterToolbar from './components/FilterToolbar'; 5import ProjectGrid from './components/ProjectGrid'; 6import { filterAndSortProjects, getTagsByNetwork } from './utils/projectUtils'; 7 8function App() { 9 const [projects, setProjects] = useState<Project[]>([]); 10 const [loading, setLoading] = useState(true); 11 const [filters, setFilters] = useState<FilterState>({ 12 network: 'atproto', 13 query: '', 14 tags: [], 15 sort: 'stars' 16 }); 17 18 useEffect(() => { 19 fetch('/data/projects.json') 20 .then(res => res.json()) 21 .then(data => { 22 setProjects(data); 23 setLoading(false); 24 }) 25 .catch(err => { 26 console.error('Failed to load projects:', err); 27 setLoading(false); 28 }); 29 }, []); 30 31 useEffect(() => { 32 const params = new URLSearchParams({ 33 network: filters.network, 34 q: filters.query, 35 tags: filters.tags.join(','), 36 sort: filters.sort 37 }); 38 const newUrl = `${window.location.pathname}?${params.toString()}`; 39 window.history.replaceState({}, '', newUrl); 40 }, [filters]); 41 42 // Calculate available tags from actual project data 43 const tagsByNetwork = useMemo(() => getTagsByNetwork(projects), [projects]); 44 const availableTags = tagsByNetwork[filters.network] || []; 45 46 const handleNetworkChange = (network: Network) => { 47 setFilters(prev => { 48 // Clear tags that aren't available in the new network 49 const newNetworkTags = tagsByNetwork[network] || []; 50 const filteredTags = prev.tags.filter(tag => 51 newNetworkTags.includes(tag) 52 ); 53 54 return { ...prev, network, tags: filteredTags }; 55 }); 56 }; 57 58 const handleSearchChange = (query: string) => { 59 setFilters(prev => ({ ...prev, query })); 60 }; 61 62 const handleTagsChange = (tags: string[]) => { 63 setFilters(prev => ({ ...prev, tags })); 64 }; 65 66 const handleSortChange = (sort: FilterState['sort']) => { 67 setFilters(prev => ({ ...prev, sort })); 68 }; 69 70 const filteredProjects = filterAndSortProjects(projects, filters); 71 72 return ( 73 <div className="min-h-screen bg-gray-900"> 74 <TopBar 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} 82 availableTags={availableTags} 83 sort={filters.sort} 84 projectCount={filteredProjects.length} 85 onSearchChange={handleSearchChange} 86 onTagsChange={handleTagsChange} 87 onSortChange={handleSortChange} 88 /> 89 <ProjectGrid 90 projects={filteredProjects} 91 loading={loading} 92 /> 93 </div> 94 </div> 95 ); 96} 97 98export default App;