This repository has no description
0

Configure Feed

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

fix

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