A project tracker for decentralized social media platforms, clients, and tools
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;