src
components
Resources
ScoringMethodology
···
291
291
background-color: rgba(var(--text-rgb), 0.2);
292
292
}
293
293
294
294
-
/* New toggle styling */
295
295
-
.new-filter {
296
296
-
margin-left: auto;
294
294
+
/* Toggle filters container - New */
295
295
+
.toggle-filters {
296
296
+
display: flex;
297
297
+
flex-wrap: wrap;
298
298
+
gap: 1.5rem;
299
299
+
width: 100%;
300
300
+
justify-content: center;
301
301
+
margin-top: 1rem;
302
302
+
}
303
303
+
304
304
+
/* New and Score toggle styling */
305
305
+
.new-filter,
306
306
+
.score-filter {
307
307
+
display: flex;
308
308
+
align-items: center;
297
309
}
298
310
299
311
.toggle-label {
···
309
321
cursor: pointer;
310
322
height: 20px;
311
323
margin: 0 8px 0 0;
312
312
-
margin-right: 8px;
324
324
+
margin-right: 8px;
313
325
position: relative;
314
326
transition: background-color .3s;
315
327
width: 38px;
···
341
353
transform: translateX(14px);
342
354
}
343
355
356
356
+
.toggle-label input[type="checkbox"]:focus {
357
357
+
outline: none;
358
358
+
box-shadow: 0 0 0 3px rgba(var(--button-bg-rgb), 0.2);
359
359
+
}
360
360
+
344
361
.toggle-text {
345
362
font-size: .95rem;
346
363
font-weight: 600;
···
431
448
/* Improved resource header styling */
432
449
.resource-header {
433
450
display: flex;
434
434
-
align-items: center;
451
451
+
align-items: flex-start;
452
452
+
justify-content: space-between;
435
453
gap: 8px;
436
454
margin-bottom: 10px;
437
455
}
···
443
461
line-height: 1.3;
444
462
}
445
463
446
446
-
/* Improved NEW badge styling */
447
447
-
.new-badge {
464
464
+
/* Resource badges container */
465
465
+
.resource-badges {
466
466
+
display: flex;
467
467
+
flex-wrap: wrap;
468
468
+
gap: 6px;
469
469
+
flex-shrink: 0;
470
470
+
margin-top: 3px;
471
471
+
}
472
472
+
473
473
+
/* Badge styling */
474
474
+
.new-badge, .score-badge {
448
475
align-items: center;
449
449
-
animation: pulse 2s infinite;
450
450
-
background-color: #666 !important;
451
476
border-radius: 5.9px;
452
477
color: var(--button-text);
453
478
display: inline-flex;
···
460
485
margin-bottom: 4px;
461
486
}
462
487
488
488
+
/* NEW badge styling */
489
489
+
.new-badge {
490
490
+
animation: pulse 2s infinite;
491
491
+
background-color: #666 !important;
492
492
+
}
493
493
+
494
494
+
/* SCORE badge styling */
495
495
+
.score-badge {
496
496
+
background-color: var(--button-bg) !important;
497
497
+
}
498
498
+
463
499
@keyframes pulse {
464
500
0% {
465
501
transform: scale(1);
···
495
531
margin-top: auto;
496
532
}
497
533
534
534
+
/* Categories display */
535
535
+
.resource-categories {
536
536
+
display: flex;
537
537
+
flex-wrap: wrap;
538
538
+
gap: 6px;
539
539
+
}
540
540
+
498
541
.resource-category {
499
542
font-size: 0.8rem;
500
543
background-color: var(--card-border);
···
503
546
color: var(--text);
504
547
}
505
548
549
549
+
/* Tags display */
550
550
+
.resource-tags {
551
551
+
display: flex;
552
552
+
flex-wrap: wrap;
553
553
+
gap: 4px;
554
554
+
margin-top: 8px;
555
555
+
}
556
556
+
557
557
+
.resource-tag {
558
558
+
font-size: 0.75rem;
559
559
+
background-color: rgba(var(--button-bg-rgb), 0.1);
560
560
+
padding: 3px 6px;
561
561
+
border-radius: 4px;
562
562
+
color: var(--text);
563
563
+
opacity: 0.8;
564
564
+
}
565
565
+
506
566
/* Enhanced quality stars */
507
567
.resource-quality {
508
568
display: flex;
···
563
623
opacity: 0.8;
564
624
}
565
625
566
566
-
#new-toggle:focus {
626
626
+
#new-toggle:focus,
627
627
+
#score-toggle:focus {
567
628
border-color: var(--card-border);
568
629
}
569
630
···
604
665
gap: 1rem;
605
666
}
606
667
668
668
+
.toggle-filters {
669
669
+
flex-direction: column;
670
670
+
align-items: center;
671
671
+
gap: 1rem;
672
672
+
}
673
673
+
607
674
.category-filter-dropdown,
608
675
.quality-filter,
609
609
-
.new-filter {
676
676
+
.new-filter,
677
677
+
.score-filter {
610
678
width: 100%;
611
679
}
612
680
···
1
1
-
// src/components/Resources/Resources.jsx - Updated for multiple categories
1
1
+
// src/components/Resources/Resources.jsx
2
2
import React, { useState, useEffect, useMemo } from 'react';
3
3
import './Resources.css';
4
4
import ResourceLoader from './ResourceLoader';
···
10
10
const [activeCategory, setActiveCategory] = useState('All');
11
11
const [searchQuery, setSearchQuery] = useState('');
12
12
const [showNewOnly, setShowNewOnly] = useState(false);
13
13
+
const [showScoreImpactOnly, setShowScoreImpactOnly] = useState(false);
13
14
const [isLoading, setIsLoading] = useState(true);
14
15
15
16
// Category emojis mapping
···
36
37
const preferences = JSON.parse(savedPreferences);
37
38
setActiveCategory(preferences.activeCategory || 'All');
38
39
setShowNewOnly(preferences.showNewOnly || false);
40
40
+
setShowScoreImpactOnly(preferences.showScoreImpactOnly || false);
39
41
} catch (error) {
40
42
console.error('Error loading preferences:', error);
41
43
}
···
46
48
useEffect(() => {
47
49
const preferences = {
48
50
activeCategory,
49
49
-
showNewOnly
51
51
+
showNewOnly,
52
52
+
showScoreImpactOnly
50
53
};
51
54
localStorage.setItem('resourcesPreferences', JSON.stringify(preferences));
52
52
-
}, [activeCategory, showNewOnly]);
55
55
+
}, [activeCategory, showNewOnly, showScoreImpactOnly]);
53
56
54
57
// Fetch resources from Supabase
55
58
useEffect(() => {
56
59
async function fetchResources() {
57
60
setIsLoading(true);
58
61
try {
59
59
-
// First fetch all resources (removed subcategories)
62
62
+
// First fetch all resources
60
63
const { data: resourcesData, error: resourcesError } = await supabase
61
64
.from('resources')
62
65
.select('*')
···
78
81
throw categoriesError;
79
82
}
80
83
84
84
+
// Then fetch the tags for each resource
85
85
+
const { data: resourceTags, error: tagsError } = await supabase
86
86
+
.from('resource_tags')
87
87
+
.select(`
88
88
+
resource_id,
89
89
+
tag:tags(id, name)
90
90
+
`);
91
91
+
92
92
+
if (tagsError) {
93
93
+
throw tagsError;
94
94
+
}
95
95
+
81
96
// Group categories by resource_id
82
97
const categoriesByResource = {};
83
98
resourceCategories.forEach(item => {
···
91
106
});
92
107
});
93
108
109
109
+
// Group tags by resource_id
110
110
+
const tagsByResource = {};
111
111
+
resourceTags.forEach(item => {
112
112
+
if (!tagsByResource[item.resource_id]) {
113
113
+
tagsByResource[item.resource_id] = [];
114
114
+
}
115
115
+
tagsByResource[item.resource_id].push({
116
116
+
id: item.tag.id,
117
117
+
name: item.tag.name
118
118
+
});
119
119
+
});
120
120
+
94
121
// Transform data to match the expected format
95
122
const formattedResources = resourcesData.map(resource => {
96
123
// Get categories for this resource
97
124
const resourceCategoryList = categoriesByResource[resource.id] || [];
125
125
+
// Get tags for this resource
126
126
+
const resourceTagList = tagsByResource[resource.id] || [];
98
127
99
128
return {
100
129
...resource,
···
102
131
category: resourceCategoryList.length > 0 ? resourceCategoryList[0].name : 'Misc',
103
132
// Store all categories
104
133
categories: resourceCategoryList,
134
134
+
// Store all tags
135
135
+
tags: resourceTagList,
105
136
emoji: resourceCategoryList.length > 0 ? resourceCategoryList[0].emoji : '🔮',
106
137
url: addUTMParameters(resource.url)
107
138
};
···
128
159
return daysDiff < 14;
129
160
};
130
161
162
162
+
// Check if a resource impacts score
163
163
+
const impactsScore = (resource) => {
164
164
+
if (!resource.tags) return false;
165
165
+
return resource.tags.some(tag => tag.name.toLowerCase() === 'score');
166
166
+
};
167
167
+
131
168
// Add UTM parameters to URLs
132
169
const addUTMParameters = (url) => {
133
170
const separator = url.includes('?') ? '&' : '?';
···
180
217
return resource.categories && resource.categories.some(cat => cat.name === categoryName);
181
218
};
182
219
183
183
-
// Filter resources based on active category, search query, and new filter
220
220
+
// Filter resources based on active category, search query, and filters
184
221
const filteredResources = useMemo(() => {
185
222
return resources.filter(resource => {
186
223
// Filter by category
···
195
232
// Filter by "new" status if the toggle is active
196
233
const newMatch = !showNewOnly || isNewResource(resource.created_at);
197
234
198
198
-
return categoryMatch && searchMatch && newMatch;
235
235
+
// Filter by "impacts score" status if the toggle is active
236
236
+
const scoreMatch = !showScoreImpactOnly || impactsScore(resource);
237
237
+
238
238
+
return categoryMatch && searchMatch && newMatch && scoreMatch;
199
239
});
200
200
-
}, [resources, activeCategory, searchQuery, showNewOnly]);
240
240
+
}, [resources, activeCategory, searchQuery, showNewOnly, showScoreImpactOnly]);
201
241
202
242
// Get featured resources
203
243
const featuredResources = useMemo(() => {
···
225
265
if (!grouped[category.name]) {
226
266
grouped[category.name] = [];
227
267
}
228
228
-
// Avoid duplicates (could happen if we process the same resource multiple times)
268
268
+
// Avoid duplicates
229
269
if (!grouped[category.name].some(r => r.id === resource.id)) {
230
270
grouped[category.name].push(resource);
231
271
}
···
296
336
</div>
297
337
</header>
298
338
299
299
-
<div className="filter-disclaimer-container">
300
300
-
339
339
+
<div className="filter-controls-container">
301
340
{/* Improved Filter Bar */}
302
302
-
<div className="resources-filters">
303
303
-
<div className="filter-options">
304
304
-
<div className="filter-dropdowns">
341
341
+
<div className="filter-bar">
342
342
+
<div className="filter-section">
305
343
{/* Category filter dropdown */}
306
306
-
<div className="category-filter-dropdown">
344
344
+
<div className="filter-dropdown">
307
345
<label htmlFor="category-select" className="filter-label">Category:</label>
308
346
<select
309
347
id="category-select"
···
319
357
</select>
320
358
</div>
321
359
322
322
-
{/* New resources toggle */}
323
323
-
<div className="new-filter">
324
324
-
<label className="toggle-label" htmlFor="new-toggle">
325
325
-
<input
326
326
-
id="new-toggle"
327
327
-
type="checkbox"
328
328
-
checked={showNewOnly}
329
329
-
onChange={() => setShowNewOnly(!showNewOnly)}
330
330
-
aria-label="Show only recently added resources"
331
331
-
/>
332
332
-
<span className="toggle-text">Recently Added</span>
333
333
-
</label>
360
360
+
{/* Toggle filters */}
361
361
+
<div className="toggle-filters">
362
362
+
{/* New resources toggle */}
363
363
+
<div className="toggle-filter">
364
364
+
<label className="toggle-label" htmlFor="new-toggle">
365
365
+
<input
366
366
+
id="new-toggle"
367
367
+
type="checkbox"
368
368
+
checked={showNewOnly}
369
369
+
onChange={() => setShowNewOnly(!showNewOnly)}
370
370
+
aria-label="Show only recently added resources"
371
371
+
/>
372
372
+
<span className="toggle-text">Recently Added</span>
373
373
+
</label>
374
374
+
</div>
375
375
+
376
376
+
{/* Score impact toggle */}
377
377
+
<div className="toggle-filter">
378
378
+
<label className="toggle-label" htmlFor="score-toggle">
379
379
+
<input
380
380
+
id="score-toggle"
381
381
+
type="checkbox"
382
382
+
checked={showScoreImpactOnly}
383
383
+
onChange={() => setShowScoreImpactOnly(!showScoreImpactOnly)}
384
384
+
aria-label="Show only resources that impact score"
385
385
+
/>
386
386
+
<span className="toggle-text">Impacts Score</span>
387
387
+
</label>
388
388
+
</div>
334
389
</div>
335
390
</div>
336
391
</div>
337
337
-
</div>
338
392
339
393
<div className="resources-disclaimer">
340
394
<div className="disclaimer-icon">⚠️</div>
341
395
<p><strong>Disclaimer:</strong> These resources are not affiliated with cred.blue or Bluesky. Use them at your own risk and exercise caution when providing access to your data.</p>
342
396
</div>
343
343
-
344
397
</div>
345
398
346
399
{/* Loading indication */}
···
359
412
key={`featured-${index}`}
360
413
resource={resource}
361
414
isNew={isNewResource(resource.created_at)}
415
415
+
impactsScore={impactsScore(resource)}
362
416
/>
363
417
))}
364
418
</div>
···
381
435
key={`${category}-${index}`}
382
436
resource={resource}
383
437
isNew={isNewResource(resource.created_at)}
438
438
+
impactsScore={impactsScore(resource)}
384
439
/>
385
440
))}
386
441
</div>
···
398
453
key={index}
399
454
resource={resource}
400
455
isNew={isNewResource(resource.created_at)}
456
456
+
impactsScore={impactsScore(resource)}
401
457
/>
402
458
))}
403
459
</div>
···
416
472
};
417
473
418
474
// ResourceCard component for displaying individual resources
419
419
-
const ResourceCard = ({ resource, isNew }) => {
475
475
+
const ResourceCard = ({ resource, isNew, impactsScore }) => {
420
476
return (
421
477
<a
422
478
href={resource.url}
···
427
483
<div className="resource-content">
428
484
<div className="resource-header">
429
485
<h3 className="resource-name">{resource.name}</h3>
430
430
-
{isNew && (
431
431
-
<span className="new-badge">NEW</span>
432
432
-
)}
486
486
+
<div className="resource-badges">
487
487
+
{isNew && (
488
488
+
<span className="new-badge">NEW</span>
489
489
+
)}
490
490
+
{impactsScore && (
491
491
+
<span className="score-badge">SCORE</span>
492
492
+
)}
493
493
+
</div>
433
494
</div>
434
495
<p className="resource-description">{resource.description}</p>
435
496
<p className="resource-domain">{resource.domain}</p>
···
25
25
margin-top: 30px;
26
26
margin-bottom: 20px;
27
27
}
28
28
-
28
28
+
29
29
.methodology-page h3 {
30
30
-
margin-top: 25px;
31
31
-
margin-bottom: 15px;
30
30
+
margin-bottom: 10px;
31
31
+
margin-top: 20px;
32
32
}
33
33
34
34
.methodology-page p {
···
38
38
39
39
.increase-score-list li {
40
40
margin-bottom: 5px;
41
41
+
}
42
42
+
43
43
+
.related-pages-title h3 {
44
44
+
margin-bottom: 10px;
45
45
+
margin-top: 20px;
41
46
}
42
47
43
48
.methodology-page-chart .score-gauge {