This repository has no description
0

Configure Feed

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

Add Wrapped Stats Feature to Profile Page

- Introduced a new section displaying yearly flushing statistics, including total flushes, days active, most frequent hour, top emoji, most flushes in a single day, and longest streak.
- Added corresponding styles for the new Wrapped Stats section in the CSS file.

+283
+170
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); 47 56 // Match Bluesky's API response format 48 57 interface ProfileData { 49 58 did: string; ··· 213 222 setTotalCount(userEntries.length); 214 223 setEmojiStats(emojiStats); 215 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 + 216 329 // Calculate statistics and chart data 217 330 if (userEntries.length > 0) { 218 331 // Calculate actual active days count (days with at least one flush) ··· 307 420 </div> 308 421 309 422 {error && <div className={styles.error}>{error}</div>} 423 + 424 + {/* Flushes Wrapped Section */} 425 + {!loading && !error && wrappedStats && ( 426 + <section className={styles.wrappedSection}> 427 + <div className={styles.wrappedHeader}> 428 + <h2 className={styles.wrappedTitle}>{handle}'s {wrappedStats.year} Flushes Roll Up</h2> 429 + <p className={styles.wrappedSubtitle}>A year in review</p> 430 + </div> 431 + 432 + <div className={styles.wrappedCards}> 433 + <div className={styles.wrappedCard}> 434 + <div className={styles.wrappedCardIcon}>🚽</div> 435 + <div className={styles.wrappedCardValue}>{wrappedStats.totalFlushes.toLocaleString()}</div> 436 + <div className={styles.wrappedCardLabel}>Total Flushes</div> 437 + </div> 438 + 439 + <div className={styles.wrappedCard}> 440 + <div className={styles.wrappedCardIcon}>📅</div> 441 + <div className={styles.wrappedCardValue}>{wrappedStats.daysActive}</div> 442 + <div className={styles.wrappedCardLabel}>Days Active</div> 443 + </div> 444 + 445 + {wrappedStats.mostFrequentHour !== null && ( 446 + <div className={styles.wrappedCard}> 447 + <div className={styles.wrappedCardIcon}>⏰</div> 448 + <div className={styles.wrappedCardValue}> 449 + {wrappedStats.mostFrequentHour === 0 ? '12' : wrappedStats.mostFrequentHour > 12 ? wrappedStats.mostFrequentHour - 12 : wrappedStats.mostFrequentHour} 450 + {wrappedStats.mostFrequentHour >= 12 ? 'PM' : 'AM'} 451 + </div> 452 + <div className={styles.wrappedCardLabel}>Most Active Time</div> 453 + </div> 454 + )} 455 + 456 + <div className={styles.wrappedCard}> 457 + <div className={styles.wrappedCardIcon}>{wrappedStats.topEmoji}</div> 458 + <div className={styles.wrappedCardValue}>{wrappedStats.topEmoji}</div> 459 + <div className={styles.wrappedCardLabel}>Top Emoji</div> 460 + </div> 461 + 462 + {wrappedStats.mostFlushesInDay > 0 && ( 463 + <div className={styles.wrappedCard}> 464 + <div className={styles.wrappedCardIcon}>🔥</div> 465 + <div className={styles.wrappedCardValue}>{wrappedStats.mostFlushesInDay}</div> 466 + <div className={styles.wrappedCardLabel}>Most in One Day</div> 467 + </div> 468 + )} 469 + 470 + {wrappedStats.longestStreak > 0 && ( 471 + <div className={styles.wrappedCard}> 472 + <div className={styles.wrappedCardIcon}>⚡</div> 473 + <div className={styles.wrappedCardValue}>{wrappedStats.longestStreak}</div> 474 + <div className={styles.wrappedCardLabel}>Day Streak</div> 475 + </div> 476 + )} 477 + </div> 478 + </section> 479 + )} 310 480 311 481 {!loading && !error && ( 312 482 <section className={styles.statsSection}>
+113
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 + padding: 2.5rem 1.5rem; 97 + margin-bottom: 2rem; 98 + box-shadow: 0 4px 12px var(--shadow-color); 99 + border: 1px solid var(--tile-border); 100 + } 101 + 102 + .wrappedHeader { 103 + text-align: center; 104 + margin-bottom: 2rem; 105 + } 106 + 107 + .wrappedTitle { 108 + font-size: 2.5rem; 109 + font-weight: 700; 110 + color: var(--primary-color); 111 + margin: 0 0 0.5rem 0; 112 + } 113 + 114 + .wrappedSubtitle { 115 + font-size: 1.1rem; 116 + color: var(--text-color); 117 + margin: 0; 118 + font-weight: 400; 119 + } 120 + 121 + .wrappedCards { 122 + display: grid; 123 + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); 124 + gap: 1.5rem; 125 + } 126 + 127 + .wrappedCard { 128 + background-color: var(--input-background); 129 + padding: 1.5rem 1rem; 130 + text-align: center; 131 + display: flex; 132 + flex-direction: column; 133 + align-items: center; 134 + justify-content: center; 135 + min-height: 160px; 136 + box-shadow: 0 2px 8px var(--shadow-color); 137 + border: 1px solid var(--tile-border); 138 + transition: transform 0.2s, box-shadow 0.2s; 139 + } 140 + 141 + .wrappedCard:hover { 142 + transform: translateY(-4px); 143 + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); 144 + } 145 + 146 + .wrappedCardIcon { 147 + font-size: 2.5rem; 148 + margin-bottom: 0.75rem; 149 + line-height: 1; 150 + } 151 + 152 + .wrappedCardValue { 153 + font-size: 2.5rem; 154 + font-weight: 700; 155 + color: var(--primary-color); 156 + margin-bottom: 0.5rem; 157 + line-height: 1.2; 158 + word-break: break-word; 159 + } 160 + 161 + .wrappedCardLabel { 162 + font-size: 0.9rem; 163 + color: var(--text-color); 164 + font-weight: 500; 165 + text-transform: uppercase; 166 + letter-spacing: 0.5px; 167 + } 168 + 169 + @media (max-width: 600px) { 170 + .wrappedSection { 171 + padding: 2rem 1rem; 172 + } 173 + 174 + .wrappedTitle { 175 + font-size: 2rem; 176 + } 177 + 178 + .wrappedSubtitle { 179 + font-size: 1rem; 180 + } 181 + 182 + .wrappedCards { 183 + grid-template-columns: repeat(2, 1fr); 184 + gap: 1rem; 185 + } 186 + 187 + .wrappedCard { 188 + padding: 1.25rem 0.75rem; 189 + min-height: 140px; 190 + } 191 + 192 + .wrappedCardIcon { 193 + font-size: 2rem; 194 + margin-bottom: 0.5rem; 195 + } 196 + 197 + .wrappedCardValue { 198 + font-size: 2rem; 199 + } 200 + 201 + .wrappedCardLabel { 202 + font-size: 0.85rem; 203 + } 204 + } 205 + 93 206 /* Stats section and chart */ 94 207 .statsSection { 95 208 background-color: var(--card-background);