|
| 1 | +import { useState, useRef, useEffect } from 'react'; |
| 2 | +import { Link } from 'react-router-dom'; |
| 3 | +import { useLanguage } from '../contexts/LanguageContext'; |
| 4 | + |
| 5 | +type CategoryProps = { |
| 6 | + isMobile?: boolean; |
| 7 | + onSelectMobile?: () => void; |
| 8 | +}; |
| 9 | + |
| 10 | +export function CategoriesDropdown({ isMobile, onSelectMobile }: CategoryProps) { |
| 11 | + const [isCategoriesMenuOpen, setIsCategoriesMenuOpen] = useState(false); |
| 12 | + const categoriesMenuRef = useRef<HTMLDivElement>(null); |
| 13 | + const { t } = useLanguage(); |
| 14 | + const [categoryKeys, setCategoryKeys] = useState<string[]>([]); |
| 15 | + const [isLoading, setIsLoading] = useState(true); |
| 16 | + |
| 17 | + // Fetch categories from API |
| 18 | + useEffect(() => { |
| 19 | + const fetchCategories = async () => { |
| 20 | + try { |
| 21 | + setIsLoading(true); |
| 22 | + const response = await fetch('/v1/hub/server_categories'); |
| 23 | + if (!response.ok) { |
| 24 | + throw new Error('Failed to fetch categories'); |
| 25 | + } |
| 26 | + const data = await response.json(); |
| 27 | + setCategoryKeys(data); |
| 28 | + } catch (error) { |
| 29 | + console.error('Error fetching categories:', error); |
| 30 | + // Show an empty menu if the API call fails |
| 31 | + setCategoryKeys([]); |
| 32 | + } finally { |
| 33 | + setIsLoading(false); |
| 34 | + } |
| 35 | + }; |
| 36 | + |
| 37 | + fetchCategories(); |
| 38 | + }, []); |
| 39 | + |
| 40 | + // Map category keys to their respective SVG icons |
| 41 | + const categoryIcons: Record<string, JSX.Element> = { |
| 42 | + "browser-automation": ( |
| 43 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 44 | + <path strokeLinecap="round" strokeLinejoin="round" d="M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 1 0-2.636 6.364M16.5 12V8.25" /> |
| 45 | + </svg> |
| 46 | + ), |
| 47 | + "cloud-platforms": ( |
| 48 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 49 | + <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15a4.5 4.5 0 0 0 4.5 4.5H18a3.75 3.75 0 0 0 1.332-7.257 3 3 0 0 0-3.758-3.848 5.25 5.25 0 0 0-10.233 2.33A4.502 4.502 0 0 0 2.25 15Z" /> |
| 50 | + </svg> |
| 51 | + ), |
| 52 | + "communication": ( |
| 53 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 54 | + <path strokeLinecap="round" strokeLinejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /> |
| 55 | + </svg> |
| 56 | + ), |
| 57 | + "databases": ( |
| 58 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 59 | + <path strokeLinecap="round" strokeLinejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /> |
| 60 | + </svg> |
| 61 | + ), |
| 62 | + "file-systems": ( |
| 63 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 64 | + <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" /> |
| 65 | + </svg> |
| 66 | + ), |
| 67 | + "knowledge-memory": ( |
| 68 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 69 | + <path strokeLinecap="round" strokeLinejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" /> |
| 70 | + </svg> |
| 71 | + ), |
| 72 | + "location-services": ( |
| 73 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 74 | + <path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> |
| 75 | + <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" /> |
| 76 | + </svg> |
| 77 | + ), |
| 78 | + "monitoring": ( |
| 79 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 80 | + <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /> |
| 81 | + </svg> |
| 82 | + ), |
| 83 | + "search": ( |
| 84 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 85 | + <path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> |
| 86 | + </svg> |
| 87 | + ), |
| 88 | + "version-control": ( |
| 89 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 90 | + <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" /> |
| 91 | + </svg> |
| 92 | + ), |
| 93 | + "integrations": ( |
| 94 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 95 | + <path strokeLinecap="round" strokeLinejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" /> |
| 96 | + </svg> |
| 97 | + ), |
| 98 | + "other-tools": ( |
| 99 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 100 | + <path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" /> |
| 101 | + </svg> |
| 102 | + ), |
| 103 | + "developer-tools": ( |
| 104 | + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2"> |
| 105 | + <path strokeLinecap="round" strokeLinejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" /> |
| 106 | + </svg> |
| 107 | + ) |
| 108 | + }; |
| 109 | + |
| 110 | + // Close the dropdown menu when selecting a category on mobile |
| 111 | + const handleCategoryClick = () => { |
| 112 | + if (isMobile && onSelectMobile) { |
| 113 | + onSelectMobile(); |
| 114 | + } |
| 115 | + setIsCategoriesMenuOpen(false); |
| 116 | + }; |
| 117 | + |
| 118 | + // Handle clicks outside the menu to close it |
| 119 | + useEffect(() => { |
| 120 | + const handleClickOutside = (event: MouseEvent) => { |
| 121 | + if ( |
| 122 | + isCategoriesMenuOpen && |
| 123 | + categoriesMenuRef.current && |
| 124 | + !categoriesMenuRef.current.contains(event.target as Node) && |
| 125 | + !(event.target as Element).closest('button.categories-toggle') |
| 126 | + ) { |
| 127 | + setIsCategoriesMenuOpen(false); |
| 128 | + } |
| 129 | + }; |
| 130 | + |
| 131 | + document.addEventListener('mousedown', handleClickOutside); |
| 132 | + |
| 133 | + return () => { |
| 134 | + document.removeEventListener('mousedown', handleClickOutside); |
| 135 | + }; |
| 136 | + }, [isCategoriesMenuOpen]); |
| 137 | + |
| 138 | + if (isMobile) { |
| 139 | + // Mobile version |
| 140 | + return ( |
| 141 | + <div className="border-b border-gray-100 pb-2 mb-2"> |
| 142 | + <div className="text-gray-700 font-medium text-base py-2 flex items-center"> |
| 143 | + <svg |
| 144 | + xmlns="http://www.w3.org/2000/svg" |
| 145 | + fill="none" |
| 146 | + viewBox="0 0 24 24" |
| 147 | + strokeWidth="1.5" |
| 148 | + stroke="currentColor" |
| 149 | + className="w-5 h-5 mr-2" |
| 150 | + > |
| 151 | + <path |
| 152 | + strokeLinecap="round" |
| 153 | + strokeLinejoin="round" |
| 154 | + d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" |
| 155 | + /> |
| 156 | + </svg> |
| 157 | + {t('nav.categories')} |
| 158 | + </div> |
| 159 | + <div className="flex flex-col space-y-2 mt-1 pl-4"> |
| 160 | + {isLoading ? ( |
| 161 | + <div className="text-gray-500 text-sm pl-2">Loading categories...</div> |
| 162 | + ) : ( |
| 163 | + categoryKeys.map((key, index) => ( |
| 164 | + <Link |
| 165 | + key={index} |
| 166 | + to={`/category/${key}`} |
| 167 | + className="text-gray-600 hover:text-indigo-600 text-sm flex items-center" |
| 168 | + onClick={handleCategoryClick} |
| 169 | + > |
| 170 | + {categoryIcons[key]} |
| 171 | + {t(`category.${key}`)} |
| 172 | + </Link> |
| 173 | + )) |
| 174 | + )} |
| 175 | + </div> |
| 176 | + </div> |
| 177 | + ); |
| 178 | + } |
| 179 | + |
| 180 | + // Desktop version |
| 181 | + return ( |
| 182 | + <div className="relative"> |
| 183 | + <button |
| 184 | + className="categories-toggle flex items-center text-gray-700 hover:text-indigo-600 font-medium" |
| 185 | + onClick={() => setIsCategoriesMenuOpen(!isCategoriesMenuOpen)} |
| 186 | + > |
| 187 | + <svg |
| 188 | + xmlns="http://www.w3.org/2000/svg" |
| 189 | + fill="none" |
| 190 | + viewBox="0 0 24 24" |
| 191 | + strokeWidth="1.5" |
| 192 | + stroke="currentColor" |
| 193 | + className="w-5 h-5 mr-1" |
| 194 | + > |
| 195 | + <path |
| 196 | + strokeLinecap="round" |
| 197 | + strokeLinejoin="round" |
| 198 | + d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" |
| 199 | + /> |
| 200 | + </svg> |
| 201 | + {t('nav.categories')} |
| 202 | + <svg |
| 203 | + xmlns="http://www.w3.org/2000/svg" |
| 204 | + fill="none" |
| 205 | + viewBox="0 0 24 24" |
| 206 | + strokeWidth="1.5" |
| 207 | + stroke="currentColor" |
| 208 | + className="w-4 h-4 ml-1" |
| 209 | + > |
| 210 | + <path |
| 211 | + strokeLinecap="round" |
| 212 | + strokeLinejoin="round" |
| 213 | + d="M19.5 8.25l-7.5 7.5-7.5-7.5" |
| 214 | + /> |
| 215 | + </svg> |
| 216 | + </button> |
| 217 | + |
| 218 | + {/* Categories dropdown menu */} |
| 219 | + {isCategoriesMenuOpen && ( |
| 220 | + <div |
| 221 | + className="absolute left-0 mt-2 w-60 bg-white rounded-md shadow-lg z-20 py-2" |
| 222 | + ref={categoriesMenuRef} |
| 223 | + > |
| 224 | + <div className="grid grid-cols-1 gap-1"> |
| 225 | + {isLoading ? ( |
| 226 | + <div className="px-4 py-2 text-sm text-gray-500">Loading categories...</div> |
| 227 | + ) : ( |
| 228 | + categoryKeys.map((key, index) => ( |
| 229 | + <Link |
| 230 | + key={index} |
| 231 | + to={`/category/${key}`} |
| 232 | + className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-indigo-600" |
| 233 | + onClick={handleCategoryClick} |
| 234 | + > |
| 235 | + <div className="flex items-center"> |
| 236 | + {categoryIcons[key]} |
| 237 | + {t(`category.${key}`)} |
| 238 | + </div> |
| 239 | + </Link> |
| 240 | + )) |
| 241 | + )} |
| 242 | + </div> |
| 243 | + </div> |
| 244 | + )} |
| 245 | + </div> |
| 246 | + ); |
| 247 | +} |
0 commit comments