A project tracker for decentralized social media platforms, clients, and tools
1import { useState, useRef, useEffect } from 'react';
2import type { FilterState } from '../types/project';
3
4interface FilterToolbarProps {
5 query: string;
6 selectedTags: string[];
7 availableTags: string[];
8 sort: FilterState['sort'];
9 projectCount: number;
10 onSearchChange: (query: string) => void;
11 onTagsChange: (tags: string[]) => void;
12 onSortChange: (sort: FilterState['sort']) => void;
13}
14
15const sortOptions: { value: FilterState['sort']; label: string }[] = [
16 { value: 'stars', label: 'Most Stars' },
17 { value: 'alphabetical', label: 'Alphabetical' }
18];
19
20export default function FilterToolbar({
21 query,
22 selectedTags,
23 availableTags,
24 sort,
25 projectCount,
26 onSearchChange,
27 onTagsChange,
28 onSortChange
29}: FilterToolbarProps) {
30 const [showTagDropdown, setShowTagDropdown] = useState(false);
31 const searchRef = useRef<HTMLInputElement>(null);
32
33 useEffect(() => {
34 const handleKeyDown = (e: KeyboardEvent) => {
35 if (e.key === '/' && document.activeElement?.tagName !== 'INPUT') {
36 e.preventDefault();
37 searchRef.current?.focus();
38 }
39 if (e.key === 'Escape') {
40 setShowTagDropdown(false);
41 }
42 };
43
44 const handleClickOutside = (e: MouseEvent) => {
45 // Check if click is outside the dropdown
46 const target = e.target as HTMLElement;
47 if (!target.closest('.tag-dropdown-container')) {
48 setShowTagDropdown(false);
49 }
50 };
51
52 window.addEventListener('keydown', handleKeyDown);
53 if (showTagDropdown) {
54 document.addEventListener('click', handleClickOutside);
55 }
56
57 return () => {
58 window.removeEventListener('keydown', handleKeyDown);
59 document.removeEventListener('click', handleClickOutside);
60 };
61 }, [showTagDropdown]);
62
63 const toggleTag = (tag: string) => {
64 if (selectedTags.includes(tag)) {
65 onTagsChange(selectedTags.filter(t => t !== tag));
66 } else {
67 onTagsChange([...selectedTags, tag]);
68 }
69 };
70
71 return (
72 <div className="mb-6 space-y-4">
73 <div className="flex flex-wrap gap-3 items-center">
74 <div className="relative flex-1 min-w-[200px] max-w-md">
75 <input
76 ref={searchRef}
77 type="text"
78 value={query}
79 onChange={(e) => onSearchChange(e.target.value)}
80 placeholder="Search projects or tags"
81 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"
82 />
83 <svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
85 </svg>
86 </div>
87
88 <select
89 value={sort}
90 onChange={(e) => onSortChange(e.target.value as FilterState['sort'])}
91 className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 focus:outline-none focus:border-blue-500"
92 >
93 {sortOptions.map(option => (
94 <option key={option.value} value={option.value}>
95 {option.label}
96 </option>
97 ))}
98 </select>
99
100 <div className="relative tag-dropdown-container">
101 <button
102 onClick={() => setShowTagDropdown(!showTagDropdown)}
103 className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 hover:bg-gray-700 focus:outline-none focus:border-blue-500 flex items-center gap-2"
104 >
105 <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
106 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
107 </svg>
108 Tags {selectedTags.length > 0 && `(${selectedTags.length})`}
109 <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
111 </svg>
112 </button>
113
114 {showTagDropdown && (
115 <div className="absolute top-full mt-2 w-64 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-20 max-h-80 overflow-y-auto">
116 <div className="p-2">
117 {availableTags.map(tag => (
118 <label key={tag} className="flex items-center px-2 py-1.5 hover:bg-gray-700 rounded cursor-pointer">
119 <input
120 type="checkbox"
121 checked={selectedTags.includes(tag)}
122 onChange={() => toggleTag(tag)}
123 className="mr-2 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500"
124 />
125 <span className="text-sm text-gray-200">{tag}</span>
126 </label>
127 ))}
128 </div>
129 </div>
130 )}
131 </div>
132
133 <span className="text-sm text-gray-400">
134 Showing {projectCount} projects
135 </span>
136 </div>
137
138 {selectedTags.length > 0 && (
139 <div className="flex flex-wrap gap-2">
140 {selectedTags.map(tag => (
141 <span
142 key={tag}
143 className="inline-flex items-center gap-1 px-3 py-1 bg-blue-900 text-blue-200 rounded-full text-sm"
144 >
145 {tag}
146 <button
147 onClick={() => toggleTag(tag)}
148 className="hover:text-white"
149 >
150 <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
151 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
152 </svg>
153 </button>
154 </span>
155 ))}
156 <button
157 onClick={() => onTagsChange([])}
158 className="text-sm text-gray-400 hover:text-gray-200"
159 >
160 Clear all
161 </button>
162 </div>
163 )}
164 </div>
165 );
166}