This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tweak resources styling

+453 -309
+37 -1
src/components/Navbar/Navbar.css
··· 81 81 transition: color 0.3s ease; 82 82 } 83 83 84 + .navbar-links ul li { 85 + margin-left: 20px; /* Consistent spacing between all links */ 86 + position: relative; 87 + display: flex; 88 + align-items: center; 89 + } 90 + 84 91 .navbar-links ul li a:hover { 85 92 color: #3B9AF8; /* Change color on hover */ 86 93 } ··· 237 244 238 245 .dropdown-trigger { 239 246 position: relative; 240 - display: inline-block; 247 + display: inline-flex; 248 + align-items: center; 241 249 white-space: nowrap; 250 + padding-right: 1.2em; /* Add padding to accommodate the triangle */ 242 251 } 243 252 244 253 .dropdown-trigger::after { 245 254 content: '▼'; 246 255 font-size: 0.6em; 256 + display: inline-block; 257 + margin-left: 0.4em; 247 258 position: absolute; 248 259 right: 0; 249 260 top: 50%; 250 261 transform: translateY(-50%); 251 262 transition: transform 0.2s ease; 252 263 } 264 + 253 265 254 266 .dropdown-container:hover .dropdown-trigger::after { 255 267 transform: translateY(-50%) rotate(180deg); ··· 302 314 color: #3B9AF8; 303 315 } 304 316 317 + .navbar-links ul li a:not(.dropdown-trigger) { 318 + padding-right: 0; 319 + display: inline-block; 320 + } 321 + 322 + 305 323 /* Dark mode adjustments for dropdown */ 306 324 .dark-mode .dropdown-menu { 307 325 background-color: var(--navbar-bg); ··· 467 485 .dropdown-container { 468 486 margin-bottom: 4px; 469 487 min-height: 32px; 488 + } 489 + } 490 + 491 + @media (max-width: 940px) { 492 + .dropdown-trigger { 493 + padding-right: 1.5em; 494 + } 495 + 496 + .dropdown-trigger::after { 497 + right: 0; 498 + } 499 + 500 + /* Ensure all links in mobile have consistent appearance */ 501 + .navbar-links ul li a { 502 + text-align: center; 503 + display: inline-flex; 504 + align-items: center; 505 + justify-content: center; 470 506 } 471 507 } 472 508
+13 -4
src/components/Navbar/Navbar.js
··· 4 4 import './Navbar.css'; 5 5 6 6 // Dropdown Menu Component 7 - const DropdownMenu = ({ title, path, items, position }) => { 7 + const DropdownMenu = ({ title, path, items }) => { 8 8 const [isOpen, setIsOpen] = useState(false); 9 9 const dropdownRef = useRef(null); 10 10 ··· 97 97 ); 98 98 }; 99 99 100 + // Regular menu item component to ensure consistent styling 101 + const MenuItem = ({ title, path }) => { 102 + return ( 103 + <li> 104 + <Link to={path}>{title}</Link> 105 + </li> 106 + ); 107 + }; 108 + 100 109 const Navbar = () => { 101 110 const { isDarkMode, toggleDarkMode } = useContext(ThemeContext); 102 111 const navigate = useNavigate(); ··· 140 149 </div> 141 150 <nav className="navbar-links"> 142 151 <ul> 143 - <DropdownMenu {...scoreDropdown} position="left" /> 144 - <li><Link to="/resources">resources</Link></li> 145 - <DropdownMenu {...aboutDropdown} position="right" /> 152 + <DropdownMenu {...scoreDropdown} /> 153 + <MenuItem title="resources" path="/resources" /> 154 + <DropdownMenu {...aboutDropdown} /> 146 155 </ul> 147 156 </nav> 148 157 </div>
+257 -186
src/components/Resources/Resources.css
··· 15 15 transition: background-color 0.3s ease, border-color 0.3s ease; 16 16 } 17 17 18 - /* Improved header structure */ 18 + /* ======= Redesigned Header Section ======= */ 19 19 .resources-header { 20 20 margin-bottom: 2rem; 21 + display: flex; 22 + flex-direction: column; 23 + gap: 1.5rem; 24 + } 25 + 26 + /* Header main section with title and tagline */ 27 + .header-main { 28 + text-align: center; 29 + margin-bottom: 0.5rem; 21 30 } 22 31 23 32 .resources-header h1 { 24 - font-size: 2rem; 25 - font-weight: bold; 26 - margin-bottom: 1rem; 33 + font-size: 2.2rem; 34 + font-weight: 700; 35 + margin-bottom: 0.5rem; 27 36 color: var(--button-bg); 28 - text-align: center; 37 + letter-spacing: -0.01em; 29 38 } 30 39 31 - .resources-intro { 32 - max-width: 800px; 33 - margin: 0 auto 1.5rem auto; 40 + .header-tagline p { 41 + font-size: 1.1rem; 42 + color: var(--text); 43 + opacity: 0.85; 44 + max-width: 600px; 45 + margin: 0 auto; 46 + line-height: 1.4; 34 47 } 35 48 36 - .resources-page ul { 37 - list-style: none; 38 - text-align: center; 39 - margin: 0 auto 1rem auto; 40 - padding: 0; 41 - width: 100%; 42 - opacity: 0.8; 49 + /* Feature cards */ 50 + .header-features { 51 + margin: 0.5rem 0 1.5rem; 43 52 } 44 53 45 - .resources-description { 46 - color: var(--text); 47 - line-height: 1.5; 48 - opacity: 0.8; 49 - transition: color 0.3s ease; 50 - text-align: center; 51 - font-size: 1.1rem; 52 - margin: 0 auto; 54 + .feature-cards { 55 + display: flex; 56 + justify-content: center; 57 + gap: 1.5rem; 58 + flex-wrap: wrap; 53 59 } 54 60 55 - /* Improved disclaimer styling */ 56 - .resources-disclaimer { 61 + .feature-card { 62 + display: flex; 63 + align-items: center; 57 64 background-color: var(--card-border); 58 - border-left: 4px solid #ffd700; 59 - padding: 12px 16px; 60 - margin: 1.5rem auto; 61 - border-radius: 4px; 62 - max-width: 900px; 65 + padding: 0.7rem 1.2rem; 66 + border-radius: 12px; 67 + transition: transform 0.2s, background-color 0.2s; 68 + } 69 + 70 + .feature-card:hover { 71 + transform: translateY(-2px); 72 + background-color: rgba(var(--button-bg-rgb), 0.1); 73 + } 74 + 75 + .feature-icon { 76 + font-size: 1.2rem; 77 + margin-right: 0.5rem; 78 + } 79 + 80 + .feature-text { 81 + font-size: 0.95rem; 82 + font-weight: 500; 83 + } 84 + 85 + /* Search and quick actions container */ 86 + .search-filters-container { 87 + display: flex; 88 + justify-content: space-between; 89 + align-items: center; 90 + gap: 1rem; 91 + margin: 0.5rem 0; 92 + } 93 + 94 + /* Improved search input */ 95 + .search-container { 96 + flex: 1; 97 + max-width: 400px; 98 + position: relative; 99 + } 100 + 101 + .search-icon { 102 + position: absolute; 103 + left: 12px; 104 + top: 50%; 105 + transform: translateY(-50%); 106 + font-size: 1rem; 107 + opacity: 0.6; 63 108 } 64 109 65 - .resources-disclaimer p { 66 - margin: 0; 67 - font-size: 0.9rem; 110 + .search-input { 111 + width: 100%; 112 + padding: 12px 12px 12px 40px; 113 + font-size: 1rem; 114 + border: 2px solid var(--card-border); 115 + border-radius: 30px; 116 + background-color: var(--navbar-bg); 68 117 color: var(--text); 118 + transition: all 0.3s ease; 69 119 } 70 120 71 - .share-button-container { 72 - text-align: center; 73 - margin: 1.5rem auto; 121 + .search-input:focus { 122 + border-color: var(--button-bg); 123 + outline: none; 124 + box-shadow: 0 0 0 3px rgba(var(--button-bg-rgb), 0.2); 74 125 } 75 126 127 + /* Quick actions */ 128 + .quick-actions { 129 + display: flex; 130 + gap: 0.5rem; 131 + } 132 + 133 + /* Share button */ 76 134 .share-button { 135 + display: flex; 136 + align-items: center; 137 + gap: 6px; 77 138 background-color: var(--button-bg); 78 139 color: var(--button-text); 79 140 padding: 10px 20px; 80 141 border: none; 81 - border-radius: 20px; 142 + border-radius: 30px; 82 143 font-size: 0.95rem; 83 144 font-weight: 600; 84 145 cursor: pointer; 85 - display: inline-flex; 86 - align-items: center; 87 - transition: background-color 0.3s ease; 146 + transition: all 0.2s ease; 88 147 } 89 148 90 149 .share-button:hover { 91 - opacity: 0.8; 150 + opacity: 0.9; 151 + transform: translateY(-2px); 92 152 } 93 153 94 - /* Improved search and filters */ 95 - .resources-filters { 96 - margin-bottom: 30px; 97 - max-width: 900px; 98 - margin-left: auto; 99 - margin-right: auto; 154 + .share-button:active { 155 + transform: translateY(0); 100 156 } 101 157 102 - .search-container { 103 - margin-bottom: 16px; 158 + .share-icon { 159 + font-size: 1.1rem; 104 160 } 105 161 106 - .search-input { 107 - width: 100%; 162 + /* Enhanced disclaimer styling */ 163 + .resources-disclaimer { 164 + display: flex; 165 + align-items: flex-start; 166 + gap: 10px; 167 + background-color: var(--card-border); 168 + border-left: 4px solid #ffd700; 108 169 padding: 14px 18px; 109 - font-size: 1rem; 110 - border: 2px solid var(--card-border); 170 + margin: 1rem 0; 111 171 border-radius: 8px; 112 - box-sizing: border-box; 113 - background-color: var(--navbar-bg); 172 + } 173 + 174 + .disclaimer-icon { 175 + font-size: 1.2rem; 176 + margin-top: 2px; 177 + } 178 + 179 + .resources-disclaimer p { 180 + margin: 0; 181 + font-size: 0.95rem; 114 182 color: var(--text); 115 - transition: border-color 0.3s ease; 183 + line-height: 1.5; 116 184 } 117 185 118 - .search-input:focus { 119 - border-color: var(--button-bg); 120 - outline: none; 121 - box-shadow: 0 0 0 2px rgba(var(--button-bg-rgb), 0.2); 186 + /* ======= Improved Filter Bar ======= */ 187 + .resources-filters { 188 + margin: 0.5rem 0 2rem; 189 + background-color: var(--navbar-bg); 190 + border: 1px solid var(--card-border); 191 + padding: 1rem; 192 + border-radius: 8px; 193 + } 194 + 195 + .filter-label { 196 + font-size: 0.95rem; 197 + font-weight: 600; 198 + margin-right: 8px; 199 + color: var(--text); 122 200 } 123 201 124 202 .filter-options { 125 203 display: flex; 126 - flex-wrap: wrap; 127 - gap: 12px; 128 204 align-items: center; 129 - justify-content: space-between; 130 205 } 131 206 132 - /* Filter dropdowns styling */ 133 207 .filter-dropdowns { 134 208 display: flex; 135 - gap: 16px; 209 + flex-wrap: wrap; 210 + gap: 1.5rem; 136 211 width: 100%; 137 212 align-items: center; 138 213 } 139 214 140 - .category-filter-dropdown { 141 - flex: 2; 142 - } 143 - 215 + .category-filter-dropdown, 144 216 .quality-filter { 145 - flex: 2; 217 + display: flex; 218 + align-items: center; 146 219 } 147 220 148 221 .filter-select { 149 - width: 100%; 150 - padding: 12px 16px; 222 + padding: 8px 32px 8px 12px; 151 223 border: 2px solid var(--card-border); 152 - border-radius: 8px; 224 + border-radius: 6px; 153 225 font-size: 0.95rem; 154 226 background-color: var(--navbar-bg); 155 227 color: var(--text); ··· 157 229 appearance: none; 158 230 background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 159 231 background-repeat: no-repeat; 160 - background-position: right 12px center; 232 + background-position: right 8px center; 161 233 background-size: 16px; 234 + transition: all 0.2s ease; 162 235 } 163 236 164 237 .filter-select:focus { 165 238 border-color: var(--button-bg); 166 239 outline: none; 240 + box-shadow: 0 0 0 3px rgba(var(--button-bg-rgb), 0.1); 167 241 } 168 242 169 - /* New star filter styling */ 170 - .quality-filter-stars { 243 + /* Star filter styling */ 244 + .star-filter-container { 171 245 display: flex; 172 246 align-items: center; 173 - gap: 8px; 247 + gap: 2px; 174 248 background-color: var(--navbar-bg); 175 249 border: 2px solid var(--card-border); 176 - border-radius: 8px; 177 - padding: 8px 12px; 178 - height: 44px; 179 - box-sizing: border-box; 180 - } 181 - 182 - .quality-filter-label { 183 - font-size: 0.95rem; 184 - font-weight: 500; 185 - color: var(--text); 186 - margin-right: 4px; 187 - } 188 - 189 - .star-filter-container { 190 - display: flex; 191 - align-items: center; 192 - gap: 3px; 250 + border-radius: 6px; 251 + padding: 3px 8px; 193 252 } 194 253 195 254 .star-filter-container .quality-star { 196 255 cursor: pointer; 197 256 font-size: 1.4rem; 198 257 transition: transform 0.1s, color 0.2s; 258 + line-height: 1; 199 259 } 200 260 201 261 .star-filter-container .quality-star:hover { 202 - transform: scale(1.1); 262 + transform: scale(1.15); 203 263 } 204 264 205 265 .star-filter-container .quality-star.filled { ··· 211 271 } 212 272 213 273 .quality-filter-clear { 214 - font-size: 0.8rem; 215 274 margin-left: 5px; 216 275 cursor: pointer; 276 + background-color: rgba(var(--text-rgb), 0.1); 217 277 color: var(--text); 218 - opacity: 0.7; 278 + width: 18px; 279 + height: 18px; 280 + border-radius: 50%; 281 + display: flex; 282 + align-items: center; 283 + justify-content: center; 284 + font-size: 0.8rem; 219 285 font-weight: bold; 286 + transition: all 0.2s ease; 220 287 } 221 288 222 289 .quality-filter-clear:hover { 223 - opacity: 1; 290 + background-color: rgba(var(--text-rgb), 0.2); 291 + } 292 + 293 + /* New toggle styling */ 294 + .new-filter { 295 + margin-left: auto; 296 + } 297 + 298 + .toggle-label { 299 + display: flex; 300 + align-items: center; 301 + cursor: pointer; 302 + } 303 + 304 + .toggle-label input[type="checkbox"] { 305 + position: relative; 306 + width: 38px; 307 + height: 20px; 308 + margin: 0; 309 + margin-right: 8px; 310 + appearance: none; 311 + background-color: var(--card-border); 312 + border-radius: 20px; 313 + transition: background-color 0.3s; 314 + cursor: pointer; 315 + } 316 + 317 + .toggle-label input[type="checkbox"]:checked { 318 + background-color: var(--button-bg); 319 + } 320 + 321 + .toggle-label input[type="checkbox"]::before { 322 + content: ''; 323 + position: absolute; 324 + width: 16px; 325 + height: 16px; 326 + border-radius: 50%; 327 + top: 2px; 328 + left: 2px; 329 + background-color: white; 330 + transition: transform 0.3s; 331 + } 332 + 333 + .toggle-label input[type="checkbox"]:checked::before { 334 + transform: translateX(18px); 335 + } 336 + 337 + .toggle-text { 338 + font-size: 0.95rem; 339 + font-weight: 500; 224 340 } 225 341 342 + /* ======= Content Sections ======= */ 226 343 .featured-section, 227 344 .all-resources-section { 228 345 margin-bottom: 40px; ··· 265 382 gap: 20px; 266 383 } 267 384 385 + /* ======= Resource Cards ======= */ 268 386 .resource-card { 269 387 border: 1px solid var(--card-border); 270 388 border-radius: 8px; 271 389 overflow: hidden; 272 - transition: transform 0.2s ease, box-shadow 0.2s ease; 390 + transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s; 273 391 text-decoration: none; 274 392 color: inherit; 275 393 background-color: var(--navbar-bg); ··· 318 436 font-weight: 700; 319 437 height: 18px; 320 438 padding: 2px 8px; 321 - padding-bottom: 2px; 439 + padding-bottom: 0px; 322 440 vertical-align: text-bottom; 323 441 margin-bottom: 4px; 324 - padding-bottom: 0px; 325 442 } 326 443 327 444 @keyframes pulse { ··· 375 492 .quality-star { 376 493 font-size: 1.4em; 377 494 font-weight: 700; 378 - text-shadow: 0 1px 1px #0000001a; 495 + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 379 496 } 380 497 381 498 .quality-star.filled { ··· 427 544 opacity: 0.8; 428 545 } 429 546 430 - /* New filter toggle styling */ 431 - .new-filter { 432 - flex: 1; 433 - display: flex; 434 - align-items: center; 435 - justify-content: flex-end; 547 + @keyframes spin { 548 + 0% { transform: rotate(0deg); } 549 + 100% { transform: rotate(360deg); } 436 550 } 437 551 438 - .toggle-label { 439 - display: flex; 440 - align-items: center; 441 - cursor: pointer; 442 - } 443 - 444 - .toggle-label input[type="checkbox"] { 445 - margin-right: 8px; 446 - appearance: none; 447 - position: relative; 448 - width: 40px; 449 - height: 20px; 450 - background-color: var(--card-border); 451 - border-radius: 20px; 452 - transition: background-color 0.3s; 453 - cursor: pointer; 454 - } 455 - 456 - .toggle-label input[type="checkbox"]:checked { 457 - background-color: var(--button-bg); 458 - } 459 - 460 - .toggle-label input[type="checkbox"]::before { 461 - content: ''; 462 - position: absolute; 463 - width: 16px; 464 - height: 16px; 465 - border-radius: 50%; 466 - top: 2px; 467 - left: 2px; 468 - background-color: white; 469 - transition: transform 0.3s; 470 - } 471 - 472 - .toggle-label input[type="checkbox"]:checked::before { 473 - transform: translateX(20px); 474 - } 475 - 476 - .toggle-text { 477 - font-size: 0.9rem; 478 - font-weight: 500; 479 - } 480 - 481 - /* Make filters more responsive on mobile */ 552 + /* Responsive adjustments */ 482 553 @media (max-width: 768px) { 483 - .filter-options .filter-dropdowns { 554 + .resources-page { 555 + padding: 10px; 556 + } 557 + 558 + .resources-page .alt-card { 559 + padding: 1rem; 560 + } 561 + 562 + .feature-cards { 563 + flex-direction: column; 564 + gap: 0.8rem; 565 + align-items: center; 566 + } 567 + 568 + .search-filters-container { 569 + flex-direction: column; 570 + align-items: stretch; 571 + } 572 + 573 + .search-container { 574 + max-width: none; 575 + } 576 + 577 + .filter-dropdowns { 484 578 flex-direction: column; 485 579 align-items: flex-start; 486 - gap: 12px; 580 + gap: 1rem; 487 581 } 488 582 489 583 .category-filter-dropdown, ··· 496 590 width: 100%; 497 591 } 498 592 499 - .new-filter { 500 - justify-content: flex-start; 593 + .category-filter-dropdown, 594 + .quality-filter { 595 + flex-direction: column; 596 + align-items: flex-start; 597 + gap: 0.5rem; 501 598 } 502 599 503 - .quality-filter-stars { 600 + .star-filter-container { 504 601 width: 100%; 505 602 justify-content: space-between; 506 603 } 507 - } 508 - 509 - @keyframes spin { 510 - 0% { transform: rotate(0deg); } 511 - 100% { transform: rotate(360deg); } 512 - } 513 - 514 - /* Responsive adjustments */ 515 - @media (max-width: 768px) { 516 - .resources-page { 517 - padding: 10px; 604 + 605 + .resources-disclaimer { 606 + flex-direction: column; 518 607 } 519 608 520 - .resources-page .alt-card { 521 - padding: 1rem; 522 - } 523 - 524 609 .resources-header h1 { 525 610 font-size: 1.5rem; 526 - } 527 - 528 - .resources-header { 529 - flex-direction: column; 530 - align-items: flex-start; 531 - } 532 - 533 - .share-button-container { 534 - margin-top: 16px; 535 - } 536 - 537 - .filter-dropdowns { 538 - flex-direction: column; 539 - gap: 10px; 540 611 } 541 612 542 613 .resources-grid {
+146 -118
src/components/Resources/Resources.js
··· 86 86 } catch (error) { 87 87 console.error('Error fetching resources:', error); 88 88 // In case of error, we could use local data as fallback 89 - // setResources(localResourcesWithUTM); 90 89 } finally { 91 90 setIsLoading(false); 92 91 } ··· 179 178 return grouped; 180 179 }, [filteredResources, activeCategory]); 181 180 182 - // Should show featured section only when All category is selected 183 - const shouldShowFeatured = activeCategory === 'All'; 181 + // Should show featured section only when All category is selected and no quality filter is active 182 + const shouldShowFeatured = activeCategory === 'All' && qualityFilter === 0; 184 183 185 184 // Handle star rating click for quality filter 186 185 const handleStarClick = (rating) => { ··· 190 189 return ( 191 190 <main className="resources-page"> 192 191 <div className="alt-card"> 193 - <div className="resources-header"> 194 - <h1>Bluesky Resources</h1> 192 + {/* Redesigned Header Section */} 193 + <header className="resources-header"> 194 + <div className="header-main"> 195 + <h1>Bluesky Resources</h1> 196 + <div className="header-tagline"> 197 + <p>A curated collection of tools and services for the Bluesky ecosystem</p> 198 + </div> 199 + </div> 195 200 196 - {/* Improved header structure */} 197 - <div className="resources-intro"> 198 - <ul> 199 - <li>Find tools to enhance your Bluesky experience.</li> 200 - <li>Discover analytics, feeds, clients, and more.</li> 201 - <li>Explore community-built solutions.</li> 202 - </ul> 203 - <p className="resources-description"> 204 - A curated collection of third-party tools, services, and guides for the Bluesky ecosystem 205 - </p> 201 + <div className="header-features"> 202 + <div className="feature-cards"> 203 + <div className="feature-card"> 204 + <span className="feature-icon">🔍</span> 205 + <span className="feature-text">Discover analytics, feeds & clients</span> 206 + </div> 207 + <div className="feature-card"> 208 + <span className="feature-icon">⚡</span> 209 + <span className="feature-text">Enhance your Bluesky experience</span> 210 + </div> 211 + <div className="feature-card"> 212 + <span className="feature-icon">🧩</span> 213 + <span className="feature-text">Community-built solutions</span> 214 + </div> 215 + </div> 206 216 </div> 207 217 208 - {/* Improved disclaimer positioning */} 218 + <div className="search-filters-container"> 219 + <div className="search-container"> 220 + <span className="search-icon">🔎</span> 221 + <input 222 + type="text" 223 + placeholder="Search resources..." 224 + value={searchQuery} 225 + onChange={(e) => setSearchQuery(e.target.value)} 226 + className="search-input" 227 + aria-label="Search resources" 228 + /> 229 + </div> 230 + 231 + <div className="quick-actions"> 232 + <button 233 + className="share-button" 234 + type="button" 235 + onClick={shareOnBluesky} 236 + aria-label="Share this page on Bluesky" 237 + > 238 + <span className="share-icon">📤</span> 239 + <span>Share</span> 240 + </button> 241 + </div> 242 + </div> 243 + 209 244 <div className="resources-disclaimer"> 245 + <div className="disclaimer-icon">⚠️</div> 210 246 <p><strong>Disclaimer:</strong> These resources are third-party tools and services not affiliated with cred.blue or Bluesky. 211 247 Use them at your own risk and exercise caution when providing access to your data.</p> 212 248 </div> 213 - 214 - <div className="share-button-container"> 215 - <button 216 - className="share-button" 217 - type="button" 218 - onClick={shareOnBluesky} 219 - > 220 - Share This Page 221 - </button> 222 - </div> 223 - </div> 249 + </header> 224 250 225 - {isLoading ? ( 226 - <ResourceLoader /> 227 - ) : ( 228 - <> 229 - {/* Improved search and filters layout */} 251 + {/* Improved Filter Bar */} 230 252 <div className="resources-filters"> 231 - <div className="search-container"> 232 - <input 233 - type="text" 234 - placeholder="Search resources..." 235 - value={searchQuery} 236 - onChange={(e) => setSearchQuery(e.target.value)} 237 - className="search-input" 238 - /> 239 - </div> 240 - 241 253 <div className="filter-options"> 242 254 <div className="filter-dropdowns"> 243 255 {/* Category filter dropdown */} 244 256 <div className="category-filter-dropdown"> 257 + <label htmlFor="category-select" className="filter-label">Category:</label> 245 258 <select 259 + id="category-select" 246 260 value={activeCategory} 247 261 onChange={(e) => setActiveCategory(e.target.value)} 248 262 className="filter-select" ··· 255 269 </select> 256 270 </div> 257 271 258 - {/* New Quality Filter using Stars */} 272 + {/* Quality Filter using Stars */} 259 273 <div className="quality-filter"> 260 - <div className="quality-filter-stars"> 261 - <span className="quality-filter-label">Quality: </span> 262 - <div className="star-filter-container"> 263 - {[1, 2, 3, 4, 5].map((rating) => ( 264 - <span 265 - key={rating} 266 - onClick={() => handleStarClick(rating)} 267 - className={`quality-star ${rating <= qualityFilter ? 'filled' : 'empty'}`} 268 - title={`${rating} stars or higher`} 269 - > 270 - 271 - </span> 272 - ))} 273 - {qualityFilter > 0 && ( 274 - <span 275 - className="quality-filter-clear" 276 - onClick={() => setQualityFilter(0)} 277 - title="Clear filter" 278 - > 279 - 280 - </span> 281 - )} 282 - </div> 274 + <span className="filter-label">Quality:</span> 275 + <div className="star-filter-container"> 276 + {[1, 2, 3, 4, 5].map((rating) => ( 277 + <span 278 + key={rating} 279 + onClick={() => handleStarClick(rating)} 280 + className={`quality-star ${rating <= qualityFilter ? 'filled' : 'empty'}`} 281 + title={`${rating} stars or higher`} 282 + role="button" 283 + tabIndex="0" 284 + aria-label={`Filter by ${rating} stars or higher`} 285 + onKeyPress={(e) => e.key === 'Enter' && handleStarClick(rating)} 286 + > 287 + 288 + </span> 289 + ))} 290 + {qualityFilter > 0 && ( 291 + <span 292 + className="quality-filter-clear" 293 + onClick={() => setQualityFilter(0)} 294 + title="Clear filter" 295 + role="button" 296 + tabIndex="0" 297 + aria-label="Clear quality filter" 298 + onKeyPress={(e) => e.key === 'Enter' && setQualityFilter(0)} 299 + > 300 + 301 + </span> 302 + )} 283 303 </div> 284 304 </div> 285 305 286 306 {/* New resources toggle */} 287 307 <div className="new-filter"> 288 - <label className="toggle-label"> 308 + <label className="toggle-label" htmlFor="new-toggle"> 289 309 <input 310 + id="new-toggle" 290 311 type="checkbox" 291 312 checked={showNewOnly} 292 313 onChange={() => setShowNewOnly(!showNewOnly)} 314 + aria-label="Show only recently added resources" 293 315 /> 294 - <span className="toggle-text">Recently Added Only</span> 316 + <span className="toggle-text">Recently Added</span> 295 317 </label> 296 318 </div> 297 319 </div> 298 320 </div> 299 321 </div> 300 322 301 - {shouldShowFeatured && featuredResources.length > 0 && ( 302 - <div className="featured-section"> 303 - <h2>Featured Resources</h2> 304 - <p className="featured-description">Hand-selected tools that we love and use regularly. These are not sponsored or paid placements.</p> 305 - <div className="resources-grid"> 306 - {featuredResources.map((resource, index) => ( 307 - <ResourceCard 308 - key={`featured-${index}`} 309 - resource={resource} 310 - isNew={isNewResource(resource.created_at)} 311 - /> 323 + {/* Loading indication */} 324 + {isLoading ? ( 325 + <ResourceLoader /> 326 + ) : ( 327 + <> 328 + {/* Featured Section - Hidden when quality filter is active */} 329 + {shouldShowFeatured && featuredResources.length > 0 && ( 330 + <div className="featured-section"> 331 + <h2>Featured Resources</h2> 332 + <p className="featured-description">Hand-selected tools that we love and use regularly. These are not sponsored or paid placements.</p> 333 + <div className="resources-grid"> 334 + {featuredResources.map((resource, index) => ( 335 + <ResourceCard 336 + key={`featured-${index}`} 337 + resource={resource} 338 + isNew={isNewResource(resource.created_at)} 339 + /> 340 + ))} 341 + </div> 342 + </div> 343 + )} 344 + 345 + {activeCategory === 'All' ? ( 346 + // When "All" is selected, show resources by category 347 + <div className="all-resources-section"> 348 + <h2>All Resources ({filteredResources.length})</h2> 349 + 350 + {Object.keys(resourcesByCategory).map(category => ( 351 + <div key={category} className="category-section"> 352 + <h3 className="category-header"> 353 + {categoryEmojis[category] || '🔹'} {category} ({resourcesByCategory[category].length}) 354 + </h3> 355 + <div className="resources-grid"> 356 + {resourcesByCategory[category].map((resource, index) => ( 357 + <ResourceCard 358 + key={`${category}-${index}`} 359 + resource={resource} 360 + isNew={isNewResource(resource.created_at)} 361 + /> 362 + ))} 363 + </div> 364 + </div> 312 365 ))} 313 366 </div> 314 - </div> 315 - )} 316 - 317 - {activeCategory === 'All' ? ( 318 - // When "All" is selected, show resources by category 319 - <div className="all-resources-section"> 320 - <h2>All Resources ({filteredResources.length})</h2> 321 - 322 - {Object.keys(resourcesByCategory).map(category => ( 323 - <div key={category} className="category-section"> 324 - <h3 className="category-header"> 325 - {categoryEmojis[category] || '🔹'} {category} ({resourcesByCategory[category].length}) 326 - </h3> 367 + ) : ( 368 + // When a specific category is selected 369 + <div className="all-resources-section"> 370 + <h2>{categoryEmojis[activeCategory] || '🔹'} {activeCategory} Resources ({filteredResources.length})</h2> 371 + {filteredResources.length > 0 ? ( 327 372 <div className="resources-grid"> 328 - {resourcesByCategory[category].map((resource, index) => ( 373 + {filteredResources.map((resource, index) => ( 329 374 <ResourceCard 330 - key={`${category}-${index}`} 375 + key={index} 331 376 resource={resource} 332 377 isNew={isNewResource(resource.created_at)} 333 378 /> 334 379 ))} 335 380 </div> 336 - </div> 337 - ))} 338 - </div> 339 - ) : ( 340 - // When a specific category is selected 341 - <div className="all-resources-section"> 342 - <h2>{categoryEmojis[activeCategory] || '🔹'} {activeCategory} Resources ({filteredResources.length})</h2> 343 - {filteredResources.length > 0 ? ( 344 - <div className="resources-grid"> 345 - {filteredResources.map((resource, index) => ( 346 - <ResourceCard 347 - key={index} 348 - resource={resource} 349 - isNew={isNewResource(resource.created_at)} 350 - /> 351 - ))} 352 - </div> 353 - ) : ( 354 - <div className="no-results"> 355 - <p>No resources found matching your filters.</p> 356 - </div> 357 - )} 358 - </div> 359 - )} 381 + ) : ( 382 + <div className="no-results"> 383 + <p>No resources found matching your filters.</p> 384 + </div> 385 + )} 386 + </div> 387 + )} 360 388 </> 361 389 )} 362 390 </div>