···
44
44
const [flushesPerDay, setFlushesPerDay] = useState<number>(0);
45
45
const [chartData, setChartData] = useState<{date: string, count: number}[]>([]);
46
46
const [emojiStats, setEmojiStats] = useState<EmojiStat[]>([]);
47
47
-
const [wrappedStats, setWrappedStats] = useState<{
48
48
-
mostFrequentHour: number | null;
49
49
-
daysActive: number;
50
50
-
totalFlushes: number;
51
51
-
topEmoji: string;
52
52
-
year: number;
53
53
-
mostFlushesInDay: number;
54
54
-
longestStreak: number;
55
55
-
} | null>(null);
56
47
// Match Bluesky's API response format
57
48
interface ProfileData {
58
49
did: string;
···
221
212
setEntries(userEntries);
222
213
setTotalCount(userEntries.length);
223
214
setEmojiStats(emojiStats);
224
224
-
225
225
-
// Calculate Wrapped stats (for current year)
226
226
-
const currentYear = new Date().getFullYear();
227
227
-
const yearEntries = userEntries.filter((entry: FlushingEntry) => {
228
228
-
const entryDate = new Date(entry.created_at);
229
229
-
return entryDate.getFullYear() === currentYear;
230
230
-
});
231
231
-
232
232
-
if (yearEntries.length > 0) {
233
233
-
// Calculate most frequent hour
234
234
-
const hourCounts = new Map<number, number>();
235
235
-
yearEntries.forEach((entry: FlushingEntry) => {
236
236
-
const date = new Date(entry.created_at);
237
237
-
const hour = date.getHours();
238
238
-
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
239
239
-
});
240
240
-
241
241
-
let mostFrequentHour: number | null = null;
242
242
-
let maxCount = 0;
243
243
-
hourCounts.forEach((count, hour) => {
244
244
-
if (count > maxCount) {
245
245
-
maxCount = count;
246
246
-
mostFrequentHour = hour;
247
247
-
}
248
248
-
});
249
249
-
250
250
-
// Calculate days active for the year and most flushes in a single day
251
251
-
const yearDateSet = new Set<string>();
252
252
-
const dayFlushCounts = new Map<string, number>();
253
253
-
yearEntries.forEach((entry: FlushingEntry) => {
254
254
-
const date = new Date(entry.created_at);
255
255
-
const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
256
256
-
yearDateSet.add(dateKey);
257
257
-
dayFlushCounts.set(dateKey, (dayFlushCounts.get(dateKey) || 0) + 1);
258
258
-
});
259
259
-
260
260
-
// Find most flushes in a single day
261
261
-
let mostFlushesInDay = 0;
262
262
-
dayFlushCounts.forEach((count) => {
263
263
-
if (count > mostFlushesInDay) {
264
264
-
mostFlushesInDay = count;
265
265
-
}
266
266
-
});
267
267
-
268
268
-
// Calculate longest streak (consecutive days with at least one flush)
269
269
-
const sortedDates = Array.from(yearDateSet).sort();
270
270
-
let longestStreak = 0;
271
271
-
let currentStreak = 0;
272
272
-
let previousDate: Date | null = null;
273
273
-
274
274
-
sortedDates.forEach((dateKey) => {
275
275
-
const currentDate = new Date(dateKey);
276
276
-
currentDate.setHours(0, 0, 0, 0);
277
277
-
278
278
-
if (previousDate === null) {
279
279
-
currentStreak = 1;
280
280
-
} else {
281
281
-
const daysDiff = Math.floor((currentDate.getTime() - previousDate.getTime()) / (1000 * 60 * 60 * 24));
282
282
-
if (daysDiff === 1) {
283
283
-
currentStreak++;
284
284
-
} else {
285
285
-
if (currentStreak > longestStreak) {
286
286
-
longestStreak = currentStreak;
287
287
-
}
288
288
-
currentStreak = 1;
289
289
-
}
290
290
-
}
291
291
-
previousDate = currentDate;
292
292
-
});
293
293
-
294
294
-
// Check if the last streak is the longest
295
295
-
if (currentStreak > longestStreak) {
296
296
-
longestStreak = currentStreak;
297
297
-
}
298
298
-
299
299
-
// Get top emoji for the year
300
300
-
const yearEmojiCounts = new Map<string, number>();
301
301
-
yearEntries.forEach((entry: FlushingEntry) => {
302
302
-
const emoji = entry.emoji?.trim() || '🚽';
303
303
-
const validEmoji = APPROVED_EMOJIS.includes(emoji) ? emoji : '🚽';
304
304
-
yearEmojiCounts.set(validEmoji, (yearEmojiCounts.get(validEmoji) || 0) + 1);
305
305
-
});
306
306
-
307
307
-
let topEmoji = '🚽';
308
308
-
let topEmojiCount = 0;
309
309
-
yearEmojiCounts.forEach((count, emoji) => {
310
310
-
if (count > topEmojiCount) {
311
311
-
topEmojiCount = count;
312
312
-
topEmoji = emoji;
313
313
-
}
314
314
-
});
315
315
-
316
316
-
setWrappedStats({
317
317
-
mostFrequentHour,
318
318
-
daysActive: yearDateSet.size,
319
319
-
totalFlushes: yearEntries.length,
320
320
-
topEmoji,
321
321
-
year: currentYear,
322
322
-
mostFlushesInDay,
323
323
-
longestStreak
324
324
-
});
325
325
-
} else {
326
326
-
setWrappedStats(null);
327
327
-
}
328
215
329
216
// Calculate statistics and chart data
330
217
if (userEntries.length > 0) {
···
341
228
const perDay = parseFloat((userEntries.length / activeDaysCount).toFixed(1));
342
229
setFlushesPerDay(perDay);
343
230
344
344
-
// Generate chart data (group by month)
345
345
-
const monthDataMap = new Map<string, number>();
231
231
+
// Generate chart data (group by day)
232
232
+
const chartDataMap = new Map<string, number>();
346
233
347
347
-
// Group entries by month
234
234
+
// Group entries by day
348
235
userEntries.forEach((entry: FlushingEntry) => {
349
236
const date = new Date(entry.created_at);
350
350
-
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
237
237
+
const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
351
238
352
352
-
monthDataMap.set(monthKey, (monthDataMap.get(monthKey) || 0) + 1);
239
239
+
if (chartDataMap.has(dateKey)) {
240
240
+
chartDataMap.set(dateKey, chartDataMap.get(dateKey)! + 1);
241
241
+
} else {
242
242
+
chartDataMap.set(dateKey, 1);
243
243
+
}
353
244
});
354
245
355
355
-
// Find the range of months from first flush to current month
356
356
-
if (userEntries.length > 0) {
357
357
-
// Find the oldest and newest entries
358
358
-
const sortedByDate = [...userEntries].sort((a, b) =>
359
359
-
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
360
360
-
);
361
361
-
const firstEntry = sortedByDate[0];
362
362
-
const firstDate = new Date(firstEntry.created_at);
363
363
-
const lastDate = new Date(); // Current date
364
364
-
365
365
-
// Generate all months in the range
366
366
-
const allMonths: {date: string, count: number}[] = [];
367
367
-
const currentMonth = new Date(firstDate.getFullYear(), firstDate.getMonth(), 1);
368
368
-
const endMonth = new Date(lastDate.getFullYear(), lastDate.getMonth(), 1);
369
369
-
370
370
-
while (currentMonth <= endMonth) {
371
371
-
const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
372
372
-
allMonths.push({
373
373
-
date: monthKey,
374
374
-
count: monthDataMap.get(monthKey) || 0
375
375
-
});
376
376
-
377
377
-
// Move to next month
378
378
-
currentMonth.setMonth(currentMonth.getMonth() + 1);
379
379
-
}
380
380
-
381
381
-
setChartData(allMonths);
382
382
-
} else {
383
383
-
setChartData([]);
384
384
-
}
246
246
+
// Convert map to array and sort by date
247
247
+
const chartDataArray = Array.from(chartDataMap.entries())
248
248
+
.map(([date, count]): {date: string, count: number} => ({ date, count }))
249
249
+
.sort((a, b) => a.date.localeCompare(b.date));
250
250
+
251
251
+
// Limit to last 30 days for chart readability
252
252
+
const limitedData = chartDataArray.slice(-30);
253
253
+
setChartData(limitedData);
385
254
} else {
386
255
setFlushesPerDay(0);
387
256
setChartData([]);
···
439
308
440
309
{error && <div className={styles.error}>{error}</div>}
441
310
442
442
-
{/* Flushes Wrapped Section */}
443
443
-
{!loading && !error && wrappedStats && (
444
444
-
<section className={styles.wrappedSection}>
445
445
-
<div className={styles.wrappedHeader}>
446
446
-
<h2 className={styles.wrappedTitle}>{handle}'s {wrappedStats.year} Flushes Roll Up</h2>
447
447
-
<p className={styles.wrappedSubtitle}>A year in review</p>
448
448
-
</div>
449
449
-
450
450
-
<div className={styles.wrappedCards}>
451
451
-
<div className={styles.wrappedCard}>
452
452
-
<div className={styles.wrappedCardValue}>{wrappedStats.totalFlushes.toLocaleString()}</div>
453
453
-
<div className={styles.wrappedCardLabel}>Total Flushes</div>
454
454
-
</div>
455
455
-
456
456
-
<div className={styles.wrappedCard}>
457
457
-
<div className={styles.wrappedCardValue}>{wrappedStats.daysActive}</div>
458
458
-
<div className={styles.wrappedCardLabel}>Days Active</div>
459
459
-
</div>
460
460
-
461
461
-
{wrappedStats.mostFrequentHour !== null && (
462
462
-
<div className={styles.wrappedCard}>
463
463
-
<div className={styles.wrappedCardValue}>
464
464
-
{wrappedStats.mostFrequentHour === 0 ? '12' : wrappedStats.mostFrequentHour > 12 ? wrappedStats.mostFrequentHour - 12 : wrappedStats.mostFrequentHour}
465
465
-
{wrappedStats.mostFrequentHour >= 12 ? 'PM' : 'AM'}
466
466
-
</div>
467
467
-
<div className={styles.wrappedCardLabel}>Most Active Time</div>
468
468
-
</div>
469
469
-
)}
470
470
-
471
471
-
<div className={styles.wrappedCard}>
472
472
-
<div className={styles.wrappedCardValue}>{wrappedStats.topEmoji}</div>
473
473
-
<div className={styles.wrappedCardLabel}>Top Emoji</div>
474
474
-
</div>
475
475
-
476
476
-
{wrappedStats.mostFlushesInDay > 0 && (
477
477
-
<div className={styles.wrappedCard}>
478
478
-
<div className={styles.wrappedCardValue}>{wrappedStats.mostFlushesInDay}</div>
479
479
-
<div className={styles.wrappedCardLabel}>Most in One Day</div>
480
480
-
</div>
481
481
-
)}
482
482
-
483
483
-
{wrappedStats.longestStreak > 0 && (
484
484
-
<div className={styles.wrappedCard}>
485
485
-
<div className={styles.wrappedCardValue}>{wrappedStats.longestStreak}</div>
486
486
-
<div className={styles.wrappedCardLabel}>Day Streak</div>
487
487
-
</div>
488
488
-
)}
489
489
-
</div>
490
490
-
</section>
491
491
-
)}
492
492
-
493
311
{!loading && !error && (
494
312
<section className={styles.statsSection}>
495
313
<h3 className={styles.statsHeader}>Flushing Statistics</h3>
···
504
322
{chartData.map((dataPoint, index) => {
505
323
// Calculate height percentage (max of 100%)
506
324
const maxCount = Math.max(...chartData.map(d => d.count));
507
507
-
const heightPercent = maxCount > 0
508
508
-
? Math.max(10, Math.min(100, (dataPoint.count / maxCount) * 100))
509
509
-
: 0;
325
325
+
const heightPercent = Math.max(10, Math.min(100, (dataPoint.count / maxCount) * 100));
510
326
511
327
return (
512
328
<div
513
329
key={index}
514
330
className={styles.chartBar}
515
331
style={{ height: `${heightPercent}%` }}
516
516
-
title={`${new Date(dataPoint.date + '-01').toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}: ${dataPoint.count} flushes`}
332
332
+
title={`${dataPoint.date}: ${dataPoint.count} flushes`}
517
333
/>
518
334
);
519
335
})}
···
521
337
522
338
<div className={styles.chartLegend}>
523
339
<span className={styles.chartLegendItem}>
524
524
-
{chartData.length > 0 ? new Date(chartData[0].date + '-01').toLocaleDateString(undefined, { month: 'short', year: 'numeric' }) : ''}
340
340
+
{chartData.length > 0 ? new Date(chartData[0].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''}
525
341
</span>
526
342
<span className={styles.chartLegendItem}>
527
527
-
{chartData.length > 0 ? new Date(chartData[chartData.length - 1].date + '-01').toLocaleDateString(undefined, { month: 'short', year: 'numeric' }) : ''}
343
343
+
{chartData.length > 0 ? new Date(chartData[chartData.length - 1].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''}
528
344
</span>
529
345
</div>
530
346
···
90
90
/* Removed transform to prevent movement */
91
91
}
92
92
93
93
-
/* Flushes Wrapped Section */
94
94
-
.wrappedSection {
95
95
-
background-color: var(--card-background);
96
96
-
border-radius: 8px;
97
97
-
padding: 1.5rem;
98
98
-
margin-bottom: 1.5rem;
99
99
-
box-shadow: 0 2px 5px var(--shadow-color);
100
100
-
border: 1px solid var(--tile-border);
101
101
-
}
102
102
-
103
103
-
.wrappedHeader {
104
104
-
text-align: center;
105
105
-
margin-bottom: 2rem;
106
106
-
}
107
107
-
108
108
-
.wrappedTitle {
109
109
-
font-size: 2.5rem;
110
110
-
font-weight: 700;
111
111
-
color: var(--primary-color);
112
112
-
margin: 0 0 0.5rem 0;
113
113
-
}
114
114
-
115
115
-
.wrappedSubtitle {
116
116
-
font-size: 1.1rem;
117
117
-
color: var(--text-color);
118
118
-
margin: 0;
119
119
-
font-weight: 400;
120
120
-
}
121
121
-
122
122
-
.wrappedCards {
123
123
-
display: grid;
124
124
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
125
125
-
gap: 1.5rem;
126
126
-
}
127
127
-
128
128
-
.wrappedCard {
129
129
-
background-color: var(--input-background);
130
130
-
border-radius: 8px;
131
131
-
padding: 1.5rem 1rem;
132
132
-
text-align: center;
133
133
-
display: flex;
134
134
-
flex-direction: column;
135
135
-
align-items: center;
136
136
-
justify-content: center;
137
137
-
min-height: 160px;
138
138
-
box-shadow: 0 2px 5px var(--shadow-color);
139
139
-
border: 1px solid var(--tile-border);
140
140
-
transition: transform 0.2s, box-shadow 0.2s;
141
141
-
}
142
142
-
143
143
-
.wrappedCard:hover {
144
144
-
transform: translateY(-4px);
145
145
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
146
146
-
}
147
147
-
148
148
-
.wrappedCardValue {
149
149
-
font-size: 2.5rem;
150
150
-
font-weight: 700;
151
151
-
color: var(--primary-color);
152
152
-
margin-bottom: 0.5rem;
153
153
-
line-height: 1.2;
154
154
-
word-break: break-word;
155
155
-
}
156
156
-
157
157
-
.wrappedCardLabel {
158
158
-
font-size: 0.9rem;
159
159
-
color: var(--text-color);
160
160
-
font-weight: 500;
161
161
-
text-transform: uppercase;
162
162
-
letter-spacing: 0.5px;
163
163
-
}
164
164
-
165
165
-
@media (max-width: 600px) {
166
166
-
.wrappedSection {
167
167
-
padding: 1.5rem;
168
168
-
}
169
169
-
170
170
-
.wrappedTitle {
171
171
-
font-size: 2rem;
172
172
-
}
173
173
-
174
174
-
.wrappedSubtitle {
175
175
-
font-size: 1rem;
176
176
-
}
177
177
-
178
178
-
.wrappedCards {
179
179
-
grid-template-columns: repeat(2, 1fr);
180
180
-
gap: 0.75rem;
181
181
-
}
182
182
-
183
183
-
.wrappedCard {
184
184
-
padding: 1rem 0.5rem;
185
185
-
min-height: 140px;
186
186
-
}
187
187
-
188
188
-
.wrappedCardValue {
189
189
-
font-size: 2rem;
190
190
-
}
191
191
-
192
192
-
.wrappedCardLabel {
193
193
-
font-size: 0.85rem;
194
194
-
}
195
195
-
}
196
196
-
197
93
/* Stats section and chart */
198
94
.statsSection {
199
95
background-color: var(--card-background);