src
components
Navbar
Resources
ScoringMethodology
···
9
9
import Leaderboard from './components/Leaderboard/Leaderboard';
10
10
import Supporter from './components/Supporter/Supporter';
11
11
import Shortcut from './components/Shortcut/Shortcut';
12
12
+
import Resources from './components/Resources/Resources';
12
13
import ScoringMethodology from './components/ScoringMethodology/ScoringMethodology';
13
14
import Terms from './components/PrivacyTerms/Terms';
14
15
import Privacy from './components/PrivacyTerms/Privacy';
···
37
38
<Route path="/newsletter" element={<Newsletter />} />
38
39
<Route path="/supporter" element={<Supporter />} />
39
40
<Route path="/leaderboard" element={<Leaderboard />} />
41
41
+
<Route path="/resources" element={<Resources />} />
40
42
<Route path="/shortcut" element={<Shortcut />} />
41
43
<Route path="/zen" element={<ZenPage />} />
42
44
<Route path="/methodology" element={<ScoringMethodology />} />
···
488
488
}
489
489
490
490
function calculateSocialStatus({ ageInDays = 0, followersCount = 0, followsCount = 0, engagementRate = 0 }) {
491
491
-
// Define engagement thresholds
492
492
-
const ENGAGEMENT_THRESHOLDS = {
493
493
-
high: 0.03, // 3%
494
494
-
moderate: 0.01, // 1%
495
495
-
low: 0.005 // 0.5%
496
496
-
};
497
497
-
498
498
-
// Calculate follow percentage
499
499
-
const followPercentage = followersCount > 0 ? followsCount / followersCount : 0;
500
500
-
501
501
-
// Determine base status
502
502
-
let baseStatus = "Explorer";
491
491
+
// Define the minimum engagement rate threshold for advancing to higher tiers
492
492
+
const MIN_ENGAGEMENT_RATE = 0.01; // 1%
503
493
504
504
-
// Check for Newcomer first
494
494
+
// Check for Newcomer first (less than 30 days old)
505
495
if (ageInDays < 30) {
506
506
-
baseStatus = "Newcomer";
496
496
+
return "Newcomer";
507
497
}
508
508
-
// Only check other statuses if not a newcomer
509
509
-
else if (followPercentage < 0.5) {
510
510
-
if (followersCount >= 100000) {
511
511
-
baseStatus = "Leader";
512
512
-
} else if (followersCount >= 10000) {
513
513
-
baseStatus = "Guide";
514
514
-
} else if (followersCount >= 1000) {
515
515
-
baseStatus = "Pathfinder";
498
498
+
499
499
+
// Default status for accounts older than 30 days
500
500
+
let status = "Explorer";
501
501
+
502
502
+
// Check follower counts and engagement rate for higher tiers
503
503
+
if (followersCount >= 100000) {
504
504
+
if (engagementRate >= MIN_ENGAGEMENT_RATE) {
505
505
+
status = "Leader";
506
506
+
} else {
507
507
+
// Fallback to Guide if engagement requirement not met
508
508
+
status = "Guide";
509
509
+
}
510
510
+
} else if (followersCount >= 10000) {
511
511
+
if (engagementRate >= MIN_ENGAGEMENT_RATE) {
512
512
+
status = "Guide";
513
513
+
} else {
514
514
+
// Fallback to Pathfinder if engagement requirement not met
515
515
+
status = "Pathfinder";
516
516
+
}
517
517
+
} else if (followersCount >= 1000) {
518
518
+
if (engagementRate >= MIN_ENGAGEMENT_RATE) {
519
519
+
status = "Pathfinder";
516
520
}
521
521
+
// Fallback to Explorer if engagement requirement not met
517
522
}
518
518
-
519
519
-
// Add engagement qualifier for all status levels
520
520
-
if (engagementRate <= ENGAGEMENT_THRESHOLDS.low) {
521
521
-
return `${baseStatus}`;
522
522
-
} else if (engagementRate <= ENGAGEMENT_THRESHOLDS.moderate) {
523
523
-
return `Engaging ${baseStatus}`;
524
524
-
} else if (engagementRate >= ENGAGEMENT_THRESHOLDS.high) {
525
525
-
return `Highly Engaging ${baseStatus}`;
523
523
+
524
524
+
// Add engagement qualifier based on rate
525
525
+
if (engagementRate > 0.03) { // 3%
526
526
+
return `Highly Engaging ${status}`;
527
527
+
} else if (engagementRate > 0.01) { // 1%
528
528
+
return `Engaging ${status}`;
526
529
}
527
527
-
528
528
-
// Return base status if engagement doesn't meet any threshold
529
529
-
return baseStatus;
530
530
+
531
531
+
// Return base status
532
532
+
return status;
530
533
}
531
534
532
535
function calculateActivityStatus(rate) {
···
26
26
<li><Link to="/">score</Link></li>
27
27
<li><Link to="/compare">compare</Link></li>
28
28
<li><Link to="/leaderboard">leaderboard</Link></li>
29
29
+
<li><Link to="/resources">resources</Link></li>
29
30
<li><Link to="/alt-text">alt text</Link></li>
30
31
<li><Link to="/about">about</Link></li>
31
32
</ul>
···
1
1
+
/* src/components/Resources/Resources.css */
2
2
+
3
3
+
.resources-page {
4
4
+
max-width: 1200px;
5
5
+
margin: 0 auto;
6
6
+
padding: 20px;
7
7
+
font-family: sans-serif;
8
8
+
}
9
9
+
10
10
+
.resources-header {
11
11
+
display: flex;
12
12
+
justify-content: space-between;
13
13
+
align-items: center;
14
14
+
margin-bottom: 20px;
15
15
+
}
16
16
+
17
17
+
.resources-title h1 {
18
18
+
font-size: 2rem;
19
19
+
margin-bottom: 8px;
20
20
+
color: #0066cc;
21
21
+
}
22
22
+
23
23
+
.resources-title p {
24
24
+
font-size: 1rem;
25
25
+
color: #666;
26
26
+
margin: 0;
27
27
+
}
28
28
+
29
29
+
.share-button-container {
30
30
+
margin-left: 20px;
31
31
+
}
32
32
+
33
33
+
.share-button {
34
34
+
background-color: #0066cc;
35
35
+
color: white;
36
36
+
padding: 10px 16px;
37
37
+
border: none;
38
38
+
border-radius: 20px;
39
39
+
font-size: 0.9rem;
40
40
+
font-weight: 600;
41
41
+
cursor: pointer;
42
42
+
display: flex;
43
43
+
align-items: center;
44
44
+
transition: background-color 0.3s ease;
45
45
+
}
46
46
+
47
47
+
.share-button:hover {
48
48
+
background-color: #0055aa;
49
49
+
}
50
50
+
51
51
+
.resources-disclaimer {
52
52
+
background-color: #f8f9fa;
53
53
+
border-left: 4px solid #ffd700;
54
54
+
padding: 12px 16px;
55
55
+
margin-bottom: 24px;
56
56
+
border-radius: 4px;
57
57
+
}
58
58
+
59
59
+
.resources-disclaimer p {
60
60
+
margin: 0;
61
61
+
font-size: 0.9rem;
62
62
+
color: #555;
63
63
+
}
64
64
+
65
65
+
.resources-filters {
66
66
+
margin-bottom: 30px;
67
67
+
}
68
68
+
69
69
+
.search-container {
70
70
+
margin-bottom: 16px;
71
71
+
}
72
72
+
73
73
+
.search-input {
74
74
+
width: 100%;
75
75
+
padding: 12px 16px;
76
76
+
font-size: 1rem;
77
77
+
border: 1px solid #ddd;
78
78
+
border-radius: 8px;
79
79
+
box-sizing: border-box;
80
80
+
}
81
81
+
82
82
+
.filter-options {
83
83
+
display: flex;
84
84
+
flex-wrap: wrap;
85
85
+
gap: 12px;
86
86
+
align-items: center;
87
87
+
justify-content: space-between;
88
88
+
}
89
89
+
90
90
+
.category-filters {
91
91
+
display: flex;
92
92
+
flex-wrap: wrap;
93
93
+
gap: 8px;
94
94
+
}
95
95
+
96
96
+
.category-filter {
97
97
+
background-color: #f0f0f0;
98
98
+
border: none;
99
99
+
padding: 8px 16px;
100
100
+
border-radius: 16px;
101
101
+
font-size: 0.9rem;
102
102
+
cursor: pointer;
103
103
+
transition: all 0.2s ease;
104
104
+
}
105
105
+
106
106
+
.category-filter:hover {
107
107
+
background-color: #e0e0e0;
108
108
+
}
109
109
+
110
110
+
.category-filter.active {
111
111
+
background-color: #0066cc;
112
112
+
color: white;
113
113
+
}
114
114
+
115
115
+
.quality-select {
116
116
+
padding: 8px 16px;
117
117
+
border: 1px solid #ddd;
118
118
+
border-radius: 8px;
119
119
+
font-size: 0.9rem;
120
120
+
}
121
121
+
122
122
+
.featured-section,
123
123
+
.all-resources-section {
124
124
+
margin-bottom: 40px;
125
125
+
}
126
126
+
127
127
+
.featured-section h2,
128
128
+
.all-resources-section h2 {
129
129
+
font-size: 1.5rem;
130
130
+
margin-bottom: 16px;
131
131
+
color: #333;
132
132
+
border-bottom: 2px solid #eee;
133
133
+
padding-bottom: 8px;
134
134
+
}
135
135
+
136
136
+
.resources-grid {
137
137
+
display: grid;
138
138
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
139
139
+
gap: 20px;
140
140
+
}
141
141
+
142
142
+
.resource-card {
143
143
+
border: 1px solid #eee;
144
144
+
border-radius: 8px;
145
145
+
overflow: hidden;
146
146
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
147
147
+
text-decoration: none;
148
148
+
color: inherit;
149
149
+
background-color: white;
150
150
+
height: 100%;
151
151
+
display: flex;
152
152
+
flex-direction: column;
153
153
+
}
154
154
+
155
155
+
.resource-card:hover {
156
156
+
transform: translateY(-4px);
157
157
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
158
158
+
}
159
159
+
160
160
+
.resource-content {
161
161
+
padding: 16px;
162
162
+
flex-grow: 1;
163
163
+
display: flex;
164
164
+
flex-direction: column;
165
165
+
}
166
166
+
167
167
+
.resource-name {
168
168
+
font-size: 1.1rem;
169
169
+
margin: 0 0 8px 0;
170
170
+
color: #0066cc;
171
171
+
}
172
172
+
173
173
+
.resource-description {
174
174
+
font-size: 0.9rem;
175
175
+
color: #555;
176
176
+
margin: 0 0 16px 0;
177
177
+
flex-grow: 1;
178
178
+
}
179
179
+
180
180
+
.resource-meta {
181
181
+
display: flex;
182
182
+
justify-content: space-between;
183
183
+
align-items: center;
184
184
+
margin-top: auto;
185
185
+
}
186
186
+
187
187
+
.resource-category {
188
188
+
font-size: 0.8rem;
189
189
+
background-color: #f0f0f0;
190
190
+
padding: 4px 8px;
191
191
+
border-radius: 4px;
192
192
+
color: #666;
193
193
+
}
194
194
+
195
195
+
.resource-quality {
196
196
+
display: flex;
197
197
+
}
198
198
+
199
199
+
.quality-star {
200
200
+
font-size: 0.9rem;
201
201
+
margin-left: 2px;
202
202
+
}
203
203
+
204
204
+
.quality-star.filled {
205
205
+
color: #ffd700;
206
206
+
}
207
207
+
208
208
+
.quality-star.empty {
209
209
+
color: #ddd;
210
210
+
}
211
211
+
212
212
+
.no-results {
213
213
+
text-align: center;
214
214
+
padding: 40px;
215
215
+
color: #666;
216
216
+
}
217
217
+
218
218
+
/* Resource Loader */
219
219
+
.resource-loader {
220
220
+
display: flex;
221
221
+
flex-direction: column;
222
222
+
align-items: center;
223
223
+
justify-content: center;
224
224
+
padding: 40px;
225
225
+
text-align: center;
226
226
+
}
227
227
+
228
228
+
.loader-spinner {
229
229
+
border: 4px solid rgba(0, 0, 0, 0.1);
230
230
+
border-left-color: #0066cc;
231
231
+
border-radius: 50%;
232
232
+
width: 40px;
233
233
+
height: 40px;
234
234
+
animation: spin 1s linear infinite;
235
235
+
margin-bottom: 16px;
236
236
+
}
237
237
+
238
238
+
@keyframes spin {
239
239
+
0% { transform: rotate(0deg); }
240
240
+
100% { transform: rotate(360deg); }
241
241
+
}
242
242
+
243
243
+
/* Responsive adjustments */
244
244
+
@media (max-width: 768px) {
245
245
+
.resources-header {
246
246
+
flex-direction: column;
247
247
+
align-items: flex-start;
248
248
+
}
249
249
+
250
250
+
.share-button-container {
251
251
+
margin: 16px 0 0 0;
252
252
+
}
253
253
+
254
254
+
.filter-options {
255
255
+
flex-direction: column;
256
256
+
align-items: flex-start;
257
257
+
}
258
258
+
259
259
+
.quality-filter {
260
260
+
width: 100%;
261
261
+
margin-top: 12px;
262
262
+
}
263
263
+
264
264
+
.quality-select {
265
265
+
width: 100%;
266
266
+
}
267
267
+
268
268
+
.resources-grid {
269
269
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
270
270
+
}
271
271
+
}
···
1
1
+
// src/components/Resources/Resources.jsx
2
2
+
import React, { useState, useEffect, useMemo } from 'react';
3
3
+
import './Resources.css';
4
4
+
import { Link } from 'react-router-dom';
5
5
+
import ResourceLoader from './ResourceLoader';
6
6
+
7
7
+
const Resources = () => {
8
8
+
// State management
9
9
+
const [activeCategory, setActiveCategory] = useState('All');
10
10
+
const [searchQuery, setSearchQuery] = useState('');
11
11
+
const [qualityFilter, setQualityFilter] = useState('All');
12
12
+
const [isLoading, setIsLoading] = useState(true);
13
13
+
14
14
+
// Resources data structure
15
15
+
const resourcesData = [
16
16
+
// Analytics & Metrics - Personal Stats
17
17
+
{
18
18
+
name: "Alt Text Rating Tool",
19
19
+
url: "https://dame.is/ratingalttext",
20
20
+
category: "Analytics",
21
21
+
subcategory: "Personal Stats",
22
22
+
description: "Check how consistently you use alt text",
23
23
+
quality: 5,
24
24
+
featured: true
25
25
+
},
26
26
+
{
27
27
+
name: "Skeet Reviewer",
28
28
+
url: "https://reviewer.skeet.tools",
29
29
+
category: "Analytics",
30
30
+
subcategory: "Personal Stats",
31
31
+
description: "Use the KonMari method to sort through your old posts",
32
32
+
quality: 5,
33
33
+
featured: true
34
34
+
},
35
35
+
{
36
36
+
name: "Venn Diagram",
37
37
+
url: "https://venn.aviva.gay/dame.bsky.social",
38
38
+
category: "Analytics",
39
39
+
subcategory: "Personal Stats",
40
40
+
description: "Visualize your social graph",
41
41
+
quality: 4,
42
42
+
featured: false
43
43
+
},
44
44
+
{
45
45
+
name: "SkyZoo",
46
46
+
url: "https://skyzoo.blue/",
47
47
+
category: "Analytics",
48
48
+
subcategory: "Personal Stats",
49
49
+
description: "Profile metrics and fun stats",
50
50
+
quality: 4,
51
51
+
featured: false
52
52
+
},
53
53
+
{
54
54
+
name: "SkyKit",
55
55
+
url: "http://skykit.blue",
56
56
+
category: "Analytics",
57
57
+
subcategory: "Personal Stats",
58
58
+
description: "Bluesky analytics",
59
59
+
quality: 4,
60
60
+
featured: true
61
61
+
},
62
62
+
63
63
+
// Analytics & Metrics - Platform Stats
64
64
+
{
65
65
+
name: "Bcounter",
66
66
+
url: "http://bcounter.nat.vg",
67
67
+
category: "Analytics",
68
68
+
subcategory: "Platform Stats",
69
69
+
description: "Realtime user growth dashboard",
70
70
+
quality: 4,
71
71
+
featured: false
72
72
+
},
73
73
+
{
74
74
+
name: "Emojistats",
75
75
+
url: "https://emojistats.bsky.sh",
76
76
+
category: "Analytics",
77
77
+
subcategory: "Platform Stats",
78
78
+
description: "Real-time emoji usage data",
79
79
+
quality: 3,
80
80
+
featured: false
81
81
+
},
82
82
+
83
83
+
// Services & AppViews
84
84
+
{
85
85
+
name: "Mutesky",
86
86
+
url: "https://mutesky.app/",
87
87
+
category: "Services",
88
88
+
subcategory: "AppViews",
89
89
+
description: "Manage your muted words in bulk",
90
90
+
quality: 4,
91
91
+
featured: false
92
92
+
},
93
93
+
{
94
94
+
name: "Frontpage",
95
95
+
url: "https://frontpage.fyi",
96
96
+
category: "Services",
97
97
+
subcategory: "AppViews",
98
98
+
description: "Decentralized link aggregator",
99
99
+
quality: 5,
100
100
+
featured: true
101
101
+
},
102
102
+
{
103
103
+
name: "Graze",
104
104
+
url: "https://www.graze.social/",
105
105
+
category: "Feeds",
106
106
+
subcategory: "Feed Tools",
107
107
+
description: "No-Code feed creator",
108
108
+
quality: 5,
109
109
+
featured: true
110
110
+
},
111
111
+
112
112
+
// Data Management
113
113
+
{
114
114
+
name: "Bulk Thread Gating",
115
115
+
url: "https://boat.kelinci.net/bsky-threadgate-applicator",
116
116
+
category: "Data",
117
117
+
subcategory: "Management",
118
118
+
description: "Bulk retroactive thread gating",
119
119
+
quality: 3,
120
120
+
featured: false
121
121
+
},
122
122
+
{
123
123
+
name: "SkySweeper",
124
124
+
url: "https://skysweeper.p8.lu",
125
125
+
category: "Data",
126
126
+
subcategory: "Management",
127
127
+
description: "Auto-delete old skeets",
128
128
+
quality: 4,
129
129
+
featured: false
130
130
+
},
131
131
+
132
132
+
// Network Management
133
133
+
{
134
134
+
name: "Network Analyzer",
135
135
+
url: "http://bsky-follow-finder.theo.io",
136
136
+
category: "Network",
137
137
+
subcategory: "Management",
138
138
+
description: "Find and analyze your network connections",
139
139
+
quality: 4,
140
140
+
featured: true
141
141
+
},
142
142
+
{
143
143
+
name: "Gentle Unfollow",
144
144
+
url: "https://bsky.cam.fyi/unfollow",
145
145
+
category: "Network",
146
146
+
subcategory: "Management",
147
147
+
description: "Track and manage who you're following",
148
148
+
quality: 4,
149
149
+
featured: true
150
150
+
},
151
151
+
152
152
+
// Alternative Clients
153
153
+
{
154
154
+
name: "deck.blue",
155
155
+
url: "http://deck.blue",
156
156
+
category: "Clients",
157
157
+
subcategory: "Alternative",
158
158
+
description: "TweetDeck for Bluesky",
159
159
+
quality: 4,
160
160
+
featured: false
161
161
+
},
162
162
+
{
163
163
+
name: "Graysky",
164
164
+
url: "https://graysky.app",
165
165
+
category: "Clients",
166
166
+
subcategory: "Alternative",
167
167
+
description: "Alternative mobile client",
168
168
+
quality: 5,
169
169
+
featured: false
170
170
+
},
171
171
+
172
172
+
// Labelers & Moderation
173
173
+
{
174
174
+
name: "US Politics Labeler",
175
175
+
url: "https://bsky.app/profile/uspol.bluesky.bot",
176
176
+
category: "Moderation",
177
177
+
subcategory: "Labelers",
178
178
+
description: "Labels political content",
179
179
+
quality: 4,
180
180
+
featured: true
181
181
+
},
182
182
+
{
183
183
+
name: "Pronouns Labeler",
184
184
+
url: "https://bsky.app/profile/pronouns.adorable.mom",
185
185
+
category: "Moderation",
186
186
+
subcategory: "Labelers",
187
187
+
description: "Adds pronoun information to profiles",
188
188
+
quality: 4,
189
189
+
featured: true
190
190
+
},
191
191
+
192
192
+
// Feeds & Discovery
193
193
+
{
194
194
+
name: "Quiet Posters",
195
195
+
url: "https://bsky.app/profile/did:plc:vpkhqolt662uhesyj6nxm7ys/feed/infreq",
196
196
+
category: "Feeds",
197
197
+
subcategory: "Discovery",
198
198
+
description: "Feed of less frequent posters",
199
199
+
quality: 3,
200
200
+
featured: false
201
201
+
},
202
202
+
203
203
+
// Visualizations
204
204
+
{
205
205
+
name: "Bluesky by the Second",
206
206
+
url: "https://sky.flikq.dev",
207
207
+
category: "Visualizations",
208
208
+
subcategory: "Firehose",
209
209
+
description: "Live visualization of the firehose",
210
210
+
quality: 3,
211
211
+
featured: false
212
212
+
},
213
213
+
{
214
214
+
name: "Final Words",
215
215
+
url: "https://deletions.bsky.bad-example.com",
216
216
+
category: "Visualizations",
217
217
+
subcategory: "Firehose",
218
218
+
description: "Glimpses of deleted posts",
219
219
+
quality: 3,
220
220
+
featured: true
221
221
+
},
222
222
+
223
223
+
// Developer Tools
224
224
+
{
225
225
+
name: "pdsls.dev",
226
226
+
url: "https://pdsls.dev/",
227
227
+
category: "Development",
228
228
+
subcategory: "Tools",
229
229
+
description: "Browse AtProto repositories",
230
230
+
quality: 5,
231
231
+
featured: true
232
232
+
},
233
233
+
{
234
234
+
name: "sdk.blue",
235
235
+
url: "http://sdk.blue",
236
236
+
category: "Development",
237
237
+
subcategory: "Tools",
238
238
+
description: "Libraries & SDKs for the AT Protocol",
239
239
+
quality: 4,
240
240
+
featured: false
241
241
+
},
242
242
+
243
243
+
// Guides & Documentation
244
244
+
{
245
245
+
name: "Verify Your Account",
246
246
+
url: "https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial",
247
247
+
category: "Guides",
248
248
+
subcategory: "Documentation",
249
249
+
description: "How to verify your Bluesky account",
250
250
+
quality: 4,
251
251
+
featured: false
252
252
+
},
253
253
+
{
254
254
+
name: "Complete Guide to Bluesky",
255
255
+
url: "https://mackuba.eu/2024/02/21/bluesky-guide/",
256
256
+
category: "Guides",
257
257
+
subcategory: "Documentation",
258
258
+
description: "Comprehensive Bluesky guide",
259
259
+
quality: 5,
260
260
+
featured: false
261
261
+
},
262
262
+
263
263
+
// Miscellaneous
264
264
+
{
265
265
+
name: "Thread Composer",
266
266
+
url: "https://bluesky-thread-composer.pages.dev",
267
267
+
category: "Misc",
268
268
+
subcategory: "Tools",
269
269
+
description: "Create and organize threads",
270
270
+
quality: 3,
271
271
+
featured: false
272
272
+
},
273
273
+
{
274
274
+
name: "Skyview",
275
275
+
url: "https://skyview.social",
276
276
+
category: "Misc",
277
277
+
subcategory: "Tools",
278
278
+
description: "Share threads with people without an account",
279
279
+
quality: 4,
280
280
+
featured: false
281
281
+
},
282
282
+
{
283
283
+
name: "down.blue",
284
284
+
url: "https://down.blue",
285
285
+
category: "Misc",
286
286
+
subcategory: "Tools",
287
287
+
description: "Video downloader",
288
288
+
quality: 3,
289
289
+
featured: false
290
290
+
}
291
291
+
];
292
292
+
293
293
+
// Add UTM parameters to all URLs
294
294
+
const resourcesWithUTM = resourcesData.map(resource => ({
295
295
+
...resource,
296
296
+
url: `${resource.url}${resource.url.includes('?') ? '&' : '?'}utm_source=cred.blue&utm_medium=resources&utm_campaign=tools_directory`
297
297
+
}));
298
298
+
299
299
+
// Function to share the resources page on Bluesky
300
300
+
const shareOnBluesky = () => {
301
301
+
const shareText = `Check out this collection of Bluesky tools and resources from cred.blue! 🔧🦋\n\nFind analytics, feeds, alternative clients, and much more to enhance your Bluesky experience.\n\nExplore the tools: https://cred.blue/resources`;
302
302
+
303
303
+
window.open(
304
304
+
`https://bsky.app/intent/compose?text=${encodeURIComponent(shareText)}`,
305
305
+
'_blank'
306
306
+
);
307
307
+
};
308
308
+
309
309
+
// Get all categories
310
310
+
const categories = ['All', ...new Set(resourcesWithUTM.map(item => item.category))];
311
311
+
312
312
+
// Filter resources based on active category, search query, and quality filter
313
313
+
const filteredResources = useMemo(() => {
314
314
+
return resourcesWithUTM.filter(resource => {
315
315
+
// Filter by category
316
316
+
const categoryMatch = activeCategory === 'All' || resource.category === activeCategory;
317
317
+
318
318
+
// Filter by search query
319
319
+
const searchMatch =
320
320
+
resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
321
321
+
resource.description.toLowerCase().includes(searchQuery.toLowerCase());
322
322
+
323
323
+
// Filter by quality
324
324
+
const qualityMatch =
325
325
+
qualityFilter === 'All' ||
326
326
+
(qualityFilter === 'High' && resource.quality >= 4) ||
327
327
+
(qualityFilter === 'Medium' && resource.quality === 3) ||
328
328
+
(qualityFilter === 'Low' && resource.quality <= 2);
329
329
+
330
330
+
return categoryMatch && searchMatch && qualityMatch;
331
331
+
});
332
332
+
}, [resourcesWithUTM, activeCategory, searchQuery, qualityFilter]);
333
333
+
334
334
+
// Get featured resources
335
335
+
const featuredResources = useMemo(() => {
336
336
+
return resourcesWithUTM.filter(resource => resource.featured);
337
337
+
}, [resourcesWithUTM]);
338
338
+
339
339
+
// Simulate loading data
340
340
+
useEffect(() => {
341
341
+
// Simulate API fetch with a timeout
342
342
+
const loadTimer = setTimeout(() => {
343
343
+
setIsLoading(false);
344
344
+
}, 800);
345
345
+
346
346
+
return () => clearTimeout(loadTimer);
347
347
+
}, []);
348
348
+
349
349
+
return (
350
350
+
<>
351
351
+
<main className="resources-page">
352
352
+
<div className="resources-header">
353
353
+
<div className="resources-title">
354
354
+
<h1>Bluesky Resources</h1>
355
355
+
<p>A curated collection of third-party tools, services, and guides for the Bluesky ecosystem</p>
356
356
+
</div>
357
357
+
358
358
+
<div className="share-button-container">
359
359
+
<button
360
360
+
className="share-button"
361
361
+
type="button"
362
362
+
onClick={shareOnBluesky}
363
363
+
>
364
364
+
Share This Page
365
365
+
</button>
366
366
+
</div>
367
367
+
</div>
368
368
+
369
369
+
<div className="resources-disclaimer">
370
370
+
<p><strong>Disclaimer:</strong> These resources are third-party tools and services not affiliated with cred.blue or Bluesky.
371
371
+
Use them at your own risk and exercise caution when providing access to your data.</p>
372
372
+
</div>
373
373
+
374
374
+
{isLoading ? (
375
375
+
<ResourceLoader />
376
376
+
) : (
377
377
+
<>
378
378
+
<div className="resources-filters">
379
379
+
<div className="search-container">
380
380
+
<input
381
381
+
type="text"
382
382
+
placeholder="Search resources..."
383
383
+
value={searchQuery}
384
384
+
onChange={(e) => setSearchQuery(e.target.value)}
385
385
+
className="search-input"
386
386
+
/>
387
387
+
</div>
388
388
+
389
389
+
<div className="filter-options">
390
390
+
<div className="category-filters">
391
391
+
{categories.map(category => (
392
392
+
<button
393
393
+
key={category}
394
394
+
className={`category-filter ${activeCategory === category ? 'active' : ''}`}
395
395
+
onClick={() => setActiveCategory(category)}
396
396
+
>
397
397
+
{category}
398
398
+
</button>
399
399
+
))}
400
400
+
</div>
401
401
+
402
402
+
<div className="quality-filter">
403
403
+
<select
404
404
+
value={qualityFilter}
405
405
+
onChange={(e) => setQualityFilter(e.target.value)}
406
406
+
className="quality-select"
407
407
+
>
408
408
+
<option value="All">All Quality Levels</option>
409
409
+
<option value="High">High Quality</option>
410
410
+
<option value="Medium">Medium Quality</option>
411
411
+
<option value="Low">Low Quality</option>
412
412
+
</select>
413
413
+
</div>
414
414
+
</div>
415
415
+
</div>
416
416
+
417
417
+
{featuredResources.length > 0 && (
418
418
+
<div className="featured-section">
419
419
+
<h2>Featured Resources</h2>
420
420
+
<div className="resources-grid">
421
421
+
{featuredResources.map((resource, index) => (
422
422
+
<ResourceCard key={`featured-${index}`} resource={resource} />
423
423
+
))}
424
424
+
</div>
425
425
+
</div>
426
426
+
)}
427
427
+
428
428
+
<div className="all-resources-section">
429
429
+
<h2>{activeCategory === 'All' ? 'All Resources' : activeCategory}</h2>
430
430
+
{filteredResources.length > 0 ? (
431
431
+
<div className="resources-grid">
432
432
+
{filteredResources.map((resource, index) => (
433
433
+
<ResourceCard key={index} resource={resource} />
434
434
+
))}
435
435
+
</div>
436
436
+
) : (
437
437
+
<div className="no-results">
438
438
+
<p>No resources found matching your filters.</p>
439
439
+
</div>
440
440
+
)}
441
441
+
</div>
442
442
+
</>
443
443
+
)}
444
444
+
</main>
445
445
+
</>
446
446
+
);
447
447
+
};
448
448
+
449
449
+
// ResourceCard component for displaying individual resources
450
450
+
const ResourceCard = ({ resource }) => {
451
451
+
// Function to render stars based on quality rating
452
452
+
const renderQualityStars = (quality) => {
453
453
+
const stars = [];
454
454
+
for (let i = 1; i <= 5; i++) {
455
455
+
stars.push(
456
456
+
<span
457
457
+
key={i}
458
458
+
className={`quality-star ${i <= quality ? 'filled' : 'empty'}`}
459
459
+
>
460
460
+
★
461
461
+
</span>
462
462
+
);
463
463
+
}
464
464
+
return stars;
465
465
+
};
466
466
+
467
467
+
return (
468
468
+
<a
469
469
+
href={resource.url}
470
470
+
target="_blank"
471
471
+
rel="noopener noreferrer"
472
472
+
className="resource-card"
473
473
+
>
474
474
+
<div className="resource-content">
475
475
+
<h3 className="resource-name">{resource.name}</h3>
476
476
+
<p className="resource-description">{resource.description}</p>
477
477
+
<div className="resource-meta">
478
478
+
<span className="resource-category">{resource.category}</span>
479
479
+
<div className="resource-quality">
480
480
+
{renderQualityStars(resource.quality)}
481
481
+
</div>
482
482
+
</div>
483
483
+
</div>
484
484
+
</a>
485
485
+
);
486
486
+
};
487
487
+
488
488
+
export default Resources;
···
71
71
{
72
72
id: "bluesky-eras",
73
73
term: "Bluesky Eras",
74
74
-
definition: "Ever since Bluesky was first incubated from within Twitter in 2019, it has been through numerous different defining eras. Each of these eras has had distinct qualities and even cultures. The main eras are as follows: 1. pre-history (early staff, advisors, friends), 2. invite-only (with the introduction of the invite system), 3. public release (anyone could create an account)",
74
74
+
definition: "Ever since Bluesky was first incubated from within Twitter in 2019, it has been through numerous different defining eras. Each of these eras has had distinct qualities and even cultures. The main eras are as follows: 1. pre-history (staff, advisors, friends), 2. invite-only (the introduction of the invite system), 3. public release (anyone could create an account)",
75
75
learnMoreLink: "https://atproto.com/guides/account-migration#updating-identity"
76
76
},
77
77
{
···
105
105
id: "newcomer",
106
106
name: "Newcomer",
107
107
description: "Accounts that are new to Bluesky or have minimal activity. These users are just getting started on the platform and beginning to build their presence. After 30 days, Newcomers become Explorers.",
108
108
-
learnMoreLink: "https://cred.blue/social-status/newcomer"
109
108
},
110
109
{
111
110
id: "explorer",
112
111
name: "Explorer",
113
112
description: "Users who are actively engaging with the platform, discovering features, and building their initial network. They have established a basic presence but are still growing their connections and potentially finding their community.",
114
114
-
learnMoreLink: "https://cred.blue/social-status/explorer"
115
113
},
116
114
{
117
115
id: "pathfinder",
118
116
name: "Pathfinder",
119
117
description: "Established users who have developed a consistent presence and are actively contributing to conversations. These accounts have a growing influence (1,000+ followers) and solid engagement within their communities.",
120
120
-
learnMoreLink: "https://cred.blue/social-status/pathfinder"
121
118
},
122
119
{
123
120
id: "guide",
124
121
name: "Guide",
125
122
description: "Well-established users who have significant influence within specific communities (10,000+ followers). They often create valuable content and maintain strong engagement with their followers.",
126
126
-
learnMoreLink: "https://cred.blue/social-status/guide"
127
123
},
128
124
{
129
125
id: "leader",
130
126
name: "Leader",
131
127
description: "Highly influential accounts with substantial followings (100,000+) and engagement. These users have a broad impact across multiple communities and consistently contribute high-value content to the platform.",
132
132
-
learnMoreLink: "https://cred.blue/social-status/leader"
133
128
}
134
129
];
135
130