This repository has no description
0

Configure Feed

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

Refactor CSS Styles and Remove Box Shadows

- Removed box-shadow properties from various CSS files to streamline the design and improve visual consistency across components.
- Updated the Profile Page to include a new section for Wrapped 2025 statistics, enhancing user engagement with detailed flushing metrics.
- Adjusted chart data generation to group by month, providing a clearer overview of flushing activity.
- Enhanced the layout and styling of the Wrapped Stats section for better user experience.

+441 -72
+74 -26
src/app/api/bluesky/stats/route.ts
··· 219 219 throw new Error(`Failed to get daily data: ${dailyError.message}`); 220 220 } 221 221 222 - // Create a map of date -> count 223 - const dailyCounts = new Map<string, number>(); 222 + console.log(`Total records fetched: ${dailyData?.length || 0}`); 223 + if (dailyData && dailyData.length > 0) { 224 + console.log(`First record: ${JSON.stringify(dailyData[0])}`); 225 + console.log(`Last record: ${JSON.stringify(dailyData[dailyData.length - 1])}`); 226 + 227 + // Check how many records have DIDs 228 + const recordsWithDid = dailyData.filter(r => r.did); 229 + console.log(`Records with DID: ${recordsWithDid.length}`); 230 + } 231 + 232 + // Create a map of month -> count 233 + const monthlyCounts = new Map<string, number>(); 224 234 225 - // Process each entry to get daily counts 226 - dailyData?.forEach(entry => { 227 - const date = new Date(entry.created_at); 228 - const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 235 + // Get the earliest and latest dates to ensure all months are included 236 + if (dailyData && dailyData.length > 0) { 237 + const dates = dailyData.map(e => new Date(e.created_at)); 238 + const minDate = new Date(Math.min(...dates.map(d => d.getTime()))); 239 + const maxDate = new Date(Math.max(...dates.map(d => d.getTime()))); 229 240 230 - if (dailyCounts.has(dateKey)) { 231 - dailyCounts.set(dateKey, (dailyCounts.get(dateKey) || 0) + 1); 232 - } else { 233 - dailyCounts.set(dateKey, 1); 241 + // Initialize all months with 0 242 + const currentMonth = new Date(minDate.getFullYear(), minDate.getMonth(), 1); 243 + const endMonth = new Date(maxDate.getFullYear(), maxDate.getMonth(), 1); 244 + 245 + while (currentMonth <= endMonth) { 246 + const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`; 247 + monthlyCounts.set(monthKey, 0); 248 + currentMonth.setMonth(currentMonth.getMonth() + 1); 234 249 } 235 - }); 250 + 251 + // Process each entry to get monthly counts 252 + dailyData.forEach(entry => { 253 + const date = new Date(entry.created_at); 254 + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 255 + 256 + if (monthlyCounts.has(monthKey)) { 257 + monthlyCounts.set(monthKey, (monthlyCounts.get(monthKey) || 0) + 1); 258 + } else { 259 + monthlyCounts.set(monthKey, 1); 260 + } 261 + }); 262 + } 236 263 237 264 // Convert to array sorted by date 238 - const chartData = Array.from(dailyCounts.entries()) 265 + const chartData = Array.from(monthlyCounts.entries()) 239 266 .map(([date, count]): {date: string, count: number} => ({ date, count })) 240 267 .sort((a, b) => a.date.localeCompare(b.date)); 241 268 242 269 // Calculate flushes per day based on actual active days 270 + // Count actual days with flushes for the flushes per day calculation 271 + const dailyCountsForAvg = new Map<string, number>(); 272 + dailyData?.forEach(entry => { 273 + const date = new Date(entry.created_at); 274 + const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 275 + 276 + if (dailyCountsForAvg.has(dateKey)) { 277 + dailyCountsForAvg.set(dateKey, (dailyCountsForAvg.get(dateKey) || 0) + 1); 278 + } else { 279 + dailyCountsForAvg.set(dateKey, 1); 280 + } 281 + }); 282 + 243 283 let flushesPerDay = 0; 244 - if (chartData.length > 0 && totalCount !== null) { 245 - // Use the number of days with at least one flush (which is the length of chartData) 246 - // This gives us the actual active days count 247 - const activeDaysCount = chartData.length; 284 + if (dailyCountsForAvg.size > 0 && totalCount !== null) { 285 + // Use the number of days with at least one flush 286 + const activeDaysCount = dailyCountsForAvg.size; 248 287 flushesPerDay = parseFloat(((totalCount || 0) / activeDaysCount).toFixed(1)); 249 288 } 250 289 ··· 253 292 const thirtyDaysAgo = new Date(); 254 293 thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); 255 294 295 + console.log(`Calculating MAF/DAF for records after: ${thirtyDaysAgo.toISOString()}`); 296 + console.log(`Total records available: ${dailyData?.length || 0}`); 297 + 256 298 // Filter records to get only those from the last 30 days 257 299 const recentRecords = dailyData?.filter(entry => 258 300 new Date(entry.created_at) >= thirtyDaysAgo 259 301 ); 260 302 303 + console.log(`Recent records (last 30 days): ${recentRecords?.length || 0}`); 304 + 261 305 // Get unique DIDs from recent records - excluding test accounts and plumber 262 306 const recentUniqueDids = new Set<string>(); 263 307 recentRecords?.forEach(entry => { ··· 272 316 273 317 let monthlyActiveFlushers = recentUniqueDids.size; 274 318 console.log(`Monthly Active Flushers (last 30 days): ${monthlyActiveFlushers}`); 319 + console.log(`Unique DIDs in recent records: ${Array.from(recentUniqueDids).join(', ')}`); 275 320 276 321 // Calculate Daily Active Flushers (DAFs) 277 322 // This is the average number of unique users who post per day over the last 30 days ··· 395 440 }) 396 441 ); 397 442 398 - // Sort by true count (descending) in case the order changed 443 + // Sort by true count (descending) in case the order changed, and filter out 0s 399 444 const leaderboard = leaderboardWithTrueCounts 445 + .filter(item => item.count > 0) // Only include users with at least 1 flush 400 446 .sort((a, b) => b.count - a.count); 447 + 448 + console.log(`Leaderboard after filtering (${leaderboard.length} users with flushes > 0)`); 401 449 402 450 // Calculate total unique flushers (count of unique DIDs) 403 451 const totalFlushers = didCounts.size; ··· 445 493 return NextResponse.json({ 446 494 totalCount, 447 495 flushesPerDay, 448 - chartData: chartData.slice(-30), // Last 30 days 496 + chartData, // Return all months 449 497 leaderboard, 450 498 plumberFlushCount, 451 499 totalFlushers, ··· 476 524 } 477 525 } 478 526 479 - // Generate mock chart data 527 + // Generate mock chart data (by month) 480 528 function generateMockChartData() { 481 529 const chartData = []; 482 530 const today = new Date(); 483 531 484 - for (let i = 29; i >= 0; i--) { 485 - const date = new Date(today); 486 - date.setDate(date.getDate() - i); 487 - const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 532 + // Generate data for the last 12 months 533 + for (let i = 11; i >= 0; i--) { 534 + const date = new Date(today.getFullYear(), today.getMonth() - i, 1); 535 + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 488 536 489 - // Random count between 1 and 5 490 - const count = Math.floor(Math.random() * 5) + 1; 537 + // Random count between 5 and 50 538 + const count = Math.floor(Math.random() * 46) + 5; 491 539 492 - chartData.push({ date: dateString, count }); 540 + chartData.push({ date: monthKey, count }); 493 541 } 494 542 495 543 return chartData;
-2
src/app/auth/login/login.module.css
··· 16 16 border-radius: 1rem; 17 17 padding: 2rem; 18 18 border: 1px solid var(--tile-border); 19 - box-shadow: 0 4px 12px var(--shadow-color); 20 19 } 21 20 22 21 .title { ··· 66 65 .input:focus { 67 66 outline: none; 68 67 border-color: var(--primary-color); 69 - box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1); 70 68 } 71 69 72 70 .hint {
+1 -6
src/app/dashboard/dashboard.module.css
··· 62 62 .card { 63 63 background: white; 64 64 border-radius: 8px; 65 - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 66 65 padding: 2rem; 67 66 } 68 67 ··· 165 164 .input:focus { 166 165 border-color: var(--primary-color); 167 166 outline: none; 168 - box-shadow: 0 0 0 2px rgba(91, 173, 240, 0.2); 169 167 } 170 168 171 169 .charCount { ··· 249 247 .submitButton:hover:not(:disabled) { 250 248 background-color: var(--secondary-color); 251 249 transform: translateY(-2px); 252 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 253 250 } 254 251 255 252 .submitButton:disabled { ··· 294 291 border: 1px solid #e1e1e1; 295 292 border-radius: 8px; 296 293 padding: 1rem; 297 - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 298 - transition: transform 0.2s, box-shadow 0.2s; 294 + transition: transform 0.2s; 299 295 } 300 296 301 297 .feedItem:hover { 302 298 transform: translateY(-2px); 303 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 304 299 } 305 300 306 301 .feedHeader {
-2
src/app/feed/feed.module.css
··· 77 77 font-size: 1.1rem; 78 78 line-height: 1.4; 79 79 text-align: center; 80 - box-shadow: 0 2px 4px var(--shadow-color); 81 80 font-weight: 500; 82 81 } 83 82 ··· 134 133 border: 1px solid var(--tile-border); 135 134 border-radius: 8px; 136 135 padding: 1rem; 137 - box-shadow: 0 2px 5px var(--shadow-color); 138 136 /* Removed transition */ 139 137 background-image: repeating-linear-gradient(0deg, var(--tile-border), var(--tile-border) 1px, transparent 1px, transparent 20px); 140 138 }
+1 -6
src/app/page.module.css
··· 185 185 .card { 186 186 background: var(--card-background); 187 187 border-radius: 8px; 188 - box-shadow: 0 2px 10px var(--shadow-color); 189 188 padding: 2rem; 190 189 } 191 190 ··· 498 497 border: 1px solid var(--tile-border); 499 498 border-radius: 8px; 500 499 padding: 1rem; 501 - box-shadow: 0 2px 5px var(--shadow-color); 502 - transition: transform 0.2s, box-shadow 0.2s; 500 + transition: transform 0.2s; 503 501 } 504 502 505 503 .feedItem:hover { 506 504 transform: translateY(-2px); 507 - box-shadow: 0 4px 8px var(--shadow-color); 508 505 } 509 506 510 507 @media (max-width: 600px) { ··· 512 509 padding: 0.75rem; 513 510 margin-bottom: 0.5rem; 514 511 border-radius: 6px; 515 - box-shadow: 0 1px 3px var(--shadow-color); 516 512 } 517 513 518 514 .feedList { ··· 521 517 522 518 .feedItem:hover { 523 519 transform: none; 524 - box-shadow: 0 1px 3px var(--shadow-color); 525 520 } 526 521 } 527 522
+237 -12
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 [wrapped2025Data, setWrapped2025Data] = useState<{ 48 + totalFlushes: number; 49 + daysActive: number; 50 + topEmoji: string; 51 + topEmojiCount: number; 52 + mostFlushesInDay: number; 53 + activeStreak: number; 54 + mostActiveMonth: string; 55 + avgStatusLength: number; 56 + mostFrequentTime: string; 57 + } | null>(null); 47 58 // Match Bluesky's API response format 48 59 interface ProfileData { 49 60 did: string; ··· 194 205 }) 195 206 .filter((entry: FlushingEntry | null): entry is FlushingEntry => entry !== null); 196 207 208 + // Filter entries for 2025 for Wrapped stats 209 + const entries2025 = userEntries.filter((entry: FlushingEntry) => { 210 + const year = new Date(entry.created_at).getFullYear(); 211 + return year === 2025; 212 + }); 213 + 214 + // Calculate Wrapped 2025 statistics 215 + if (entries2025.length > 0) { 216 + // Days active in 2025 217 + const datesSet2025 = new Set<string>(); 218 + const hourCounts = new Map<number, number>(); 219 + const monthCounts = new Map<string, number>(); 220 + const dayCounts = new Map<string, number>(); 221 + const emojiCounts2025 = new Map<string, number>(); 222 + let totalStatusLength = 0; 223 + 224 + entries2025.forEach((entry: FlushingEntry) => { 225 + const date = new Date(entry.created_at); 226 + const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 227 + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 228 + const hour = date.getHours(); 229 + 230 + datesSet2025.add(dateKey); 231 + 232 + // Track hour frequency 233 + hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1); 234 + 235 + // Track month frequency 236 + monthCounts.set(monthKey, (monthCounts.get(monthKey) || 0) + 1); 237 + 238 + // Track daily counts for max in a day 239 + dayCounts.set(dateKey, (dayCounts.get(dateKey) || 0) + 1); 240 + 241 + // Track emoji usage 242 + const emoji = entry.emoji?.trim() || '🚽'; 243 + if (APPROVED_EMOJIS.includes(emoji)) { 244 + emojiCounts2025.set(emoji, (emojiCounts2025.get(emoji) || 0) + 1); 245 + } else { 246 + emojiCounts2025.set('🚽', (emojiCounts2025.get('🚽') || 0) + 1); 247 + } 248 + 249 + // Track status length 250 + if (entry.text) { 251 + totalStatusLength += entry.text.length; 252 + } 253 + }); 254 + 255 + // Most frequent time of day 256 + let mostFrequentHour = 0; 257 + let maxHourCount = 0; 258 + hourCounts.forEach((count, hour) => { 259 + if (count > maxHourCount) { 260 + maxHourCount = count; 261 + mostFrequentHour = hour; 262 + } 263 + }); 264 + 265 + const formatHour = (hour: number) => { 266 + const period = hour >= 12 ? 'PM' : 'AM'; 267 + const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; 268 + return `${displayHour}:00 ${period}`; 269 + }; 270 + 271 + // Most active month 272 + let mostActiveMonth = ''; 273 + let maxMonthCount = 0; 274 + monthCounts.forEach((count, month) => { 275 + if (count > maxMonthCount) { 276 + maxMonthCount = count; 277 + mostActiveMonth = month; 278 + } 279 + }); 280 + 281 + const formatMonth = (monthKey: string) => { 282 + const [year, month] = monthKey.split('-'); 283 + const date = new Date(parseInt(year), parseInt(month) - 1); 284 + return date.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); 285 + }; 286 + 287 + // Most flushes in a single day 288 + const mostFlushesInDay = Math.max(...Array.from(dayCounts.values())); 289 + 290 + // Calculate active streak (consecutive days) 291 + const sortedDates = Array.from(datesSet2025).sort(); 292 + let currentStreak = 1; 293 + let maxStreak = 1; 294 + 295 + for (let i = 1; i < sortedDates.length; i++) { 296 + const prevDate = new Date(sortedDates[i - 1]); 297 + const currDate = new Date(sortedDates[i]); 298 + const diffTime = Math.abs(currDate.getTime() - prevDate.getTime()); 299 + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 300 + 301 + if (diffDays === 1) { 302 + currentStreak++; 303 + maxStreak = Math.max(maxStreak, currentStreak); 304 + } else { 305 + currentStreak = 1; 306 + } 307 + } 308 + 309 + // Top emoji 310 + let topEmoji = '🚽'; 311 + let topEmojiCount = 0; 312 + emojiCounts2025.forEach((count, emoji) => { 313 + if (count > topEmojiCount) { 314 + topEmojiCount = count; 315 + topEmoji = emoji; 316 + } 317 + }); 318 + 319 + // Average status length 320 + const avgStatusLength = entries2025.filter(e => e.text).length > 0 321 + ? Math.round(totalStatusLength / entries2025.filter(e => e.text).length) 322 + : 0; 323 + 324 + setWrapped2025Data({ 325 + totalFlushes: entries2025.length, 326 + daysActive: datesSet2025.size, 327 + topEmoji, 328 + topEmojiCount, 329 + mostFlushesInDay, 330 + activeStreak: maxStreak, 331 + mostActiveMonth: mostActiveMonth ? formatMonth(mostActiveMonth) : 'N/A', 332 + avgStatusLength, 333 + mostFrequentTime: formatHour(mostFrequentHour) 334 + }); 335 + } else { 336 + setWrapped2025Data(null); 337 + } 338 + 197 339 // Calculate emoji statistics 198 340 const emojiCounts = new Map<string, number>(); 199 341 userEntries.forEach((entry: FlushingEntry) => { ··· 228 370 const perDay = parseFloat((userEntries.length / activeDaysCount).toFixed(1)); 229 371 setFlushesPerDay(perDay); 230 372 231 - // Generate chart data (group by day) 373 + // Generate chart data (group by month) 232 374 const chartDataMap = new Map<string, number>(); 233 375 234 - // Group entries by day 376 + // Get the earliest and latest dates 377 + const dates = userEntries.map(e => new Date(e.created_at)); 378 + const minDate = new Date(Math.min(...dates.map(d => d.getTime()))); 379 + const maxDate = new Date(Math.max(...dates.map(d => d.getTime()))); 380 + 381 + // Initialize all months with 0 382 + const currentMonth = new Date(minDate.getFullYear(), minDate.getMonth(), 1); 383 + const endMonth = new Date(maxDate.getFullYear(), maxDate.getMonth(), 1); 384 + 385 + while (currentMonth <= endMonth) { 386 + const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`; 387 + chartDataMap.set(monthKey, 0); 388 + currentMonth.setMonth(currentMonth.getMonth() + 1); 389 + } 390 + 391 + // Group entries by month 235 392 userEntries.forEach((entry: FlushingEntry) => { 236 393 const date = new Date(entry.created_at); 237 - const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; 394 + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; 238 395 239 - if (chartDataMap.has(dateKey)) { 240 - chartDataMap.set(dateKey, chartDataMap.get(dateKey)! + 1); 396 + if (chartDataMap.has(monthKey)) { 397 + chartDataMap.set(monthKey, chartDataMap.get(monthKey)! + 1); 241 398 } else { 242 - chartDataMap.set(dateKey, 1); 399 + chartDataMap.set(monthKey, 1); 243 400 } 244 401 }); 245 402 246 403 // Convert map to array and sort by date 247 404 const chartDataArray = Array.from(chartDataMap.entries()) 248 - .map(([date, count]): {date: string, count: number} => ({ date, count })) 405 + .map(([month, count]): {date: string, count: number} => ({ date: month, count })) 249 406 .sort((a, b) => a.date.localeCompare(b.date)); 250 407 251 - // Limit to last 30 days for chart readability 252 - const limitedData = chartDataArray.slice(-30); 253 - setChartData(limitedData); 408 + setChartData(chartDataArray); 254 409 } else { 255 410 setFlushesPerDay(0); 256 411 setChartData([]); ··· 337 492 338 493 <div className={styles.chartLegend}> 339 494 <span className={styles.chartLegendItem}> 340 - {chartData.length > 0 ? new Date(chartData[0].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''} 495 + {chartData.length > 0 ? (() => { 496 + const [year, month] = chartData[0].date.split('-'); 497 + const date = new Date(parseInt(year), parseInt(month) - 1); 498 + return date.toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); 499 + })() : ''} 341 500 </span> 342 501 <span className={styles.chartLegendItem}> 343 - {chartData.length > 0 ? new Date(chartData[chartData.length - 1].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''} 502 + {chartData.length > 0 ? (() => { 503 + const [year, month] = chartData[chartData.length - 1].date.split('-'); 504 + const date = new Date(parseInt(year), parseInt(month) - 1); 505 + return date.toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); 506 + })() : ''} 344 507 </span> 345 508 </div> 346 509 ··· 375 538 </div> 376 539 </div> 377 540 )} 541 + </section> 542 + )} 543 + 544 + {/* Flushes Roll Up 2025 Section */} 545 + {!loading && !error && wrapped2025Data && ( 546 + <section className={styles.wrappedSection}> 547 + <h3 className={styles.wrappedHeader}>🧻 Flushes Roll Up 2025</h3> 548 + <p className={styles.wrappedSubtitle}>The year in flushes</p> 549 + 550 + <div className={styles.wrappedGrid}> 551 + <div className={styles.wrappedCard}> 552 + <div className={styles.wrappedValue}>{wrapped2025Data.totalFlushes}</div> 553 + <div className={styles.wrappedLabel}>Total Flushes</div> 554 + </div> 555 + 556 + <div className={styles.wrappedCard}> 557 + <div className={styles.wrappedValue}>{wrapped2025Data.daysActive}</div> 558 + <div className={styles.wrappedLabel}>Days Active</div> 559 + </div> 560 + 561 + <div className={styles.wrappedCard}> 562 + <div className={styles.wrappedEmoji}>{wrapped2025Data.topEmoji}</div> 563 + <div className={styles.wrappedValue}>{wrapped2025Data.topEmojiCount}×</div> 564 + <div className={styles.wrappedLabel}>Top Emoji</div> 565 + </div> 566 + 567 + <div className={styles.wrappedCard}> 568 + <div className={styles.wrappedValue}>{wrapped2025Data.mostFlushesInDay}</div> 569 + <div className={styles.wrappedLabel}>Most in One Day</div> 570 + </div> 571 + 572 + <div className={styles.wrappedCard}> 573 + <div className={styles.wrappedValue}>{wrapped2025Data.activeStreak}</div> 574 + <div className={styles.wrappedLabel}>Longest Streak</div> 575 + </div> 576 + 577 + <div className={styles.wrappedCard}> 578 + <div className={styles.wrappedValue}>{wrapped2025Data.mostActiveMonth}</div> 579 + <div className={styles.wrappedLabel}>Most Active Month</div> 580 + </div> 581 + 582 + <div className={styles.wrappedCard}> 583 + <div className={styles.wrappedValue}>{wrapped2025Data.avgStatusLength}</div> 584 + <div className={styles.wrappedLabel}>Avg. Characters</div> 585 + </div> 586 + 587 + <div className={styles.wrappedCard}> 588 + <div className={styles.wrappedValue}>{wrapped2025Data.mostFrequentTime}</div> 589 + <div className={styles.wrappedLabel}>Peak Flush Time</div> 590 + </div> 591 + </div> 592 + 593 + <button 594 + className={styles.shareWrappedButton} 595 + onClick={() => { 596 + const shareHandle = profileData?.handle || handle; 597 + const wrappedText = `🧻 My #FlushesRollUp2025:\n\n${wrapped2025Data.totalFlushes} flushes across ${wrapped2025Data.daysActive} days\nTop emoji: ${wrapped2025Data.topEmoji}\nLongest streak: ${wrapped2025Data.activeStreak} days\nMost active: ${wrapped2025Data.mostActiveMonth}\n\nSee your stats at flushes.app! 🚽`; 598 + window.open(`https://bsky.app/intent/compose?text=${encodeURIComponent(wrappedText)}`, '_blank'); 599 + }} 600 + > 601 + Share My Roll Up 602 + </button> 378 603 </section> 379 604 )} 380 605
+117 -2
src/app/profile/[handle]/profile.module.css
··· 95 95 background-color: var(--card-background); 96 96 border-radius: 8px; 97 97 padding: 1.5rem; 98 - margin-bottom: 1.5rem; 99 - box-shadow: 0 2px 5px var(--shadow-color); 98 + margin-bottom: 1.5rem; 100 99 border: 1px solid var(--tile-border); 101 100 } 102 101 ··· 540 539 background-color: var(--background-color); 541 540 border-radius: 8px; 542 541 border: 1px dashed var(--tile-border); 542 + } 543 + 544 + /* Flushes Roll Up 2025 Section */ 545 + .wrappedSection { 546 + background-color: var(--card-background); 547 + border-radius: 8px; 548 + padding: 2rem; 549 + margin-bottom: 1.5rem; 550 + border: 1px solid var(--tile-border); 551 + } 552 + 553 + .wrappedHeader { 554 + font-size: 1.8rem; 555 + font-weight: 700; 556 + margin: 0 0 0.5rem 0; 557 + color: var(--primary-color); 558 + text-align: center; 559 + } 560 + 561 + .wrappedSubtitle { 562 + font-size: 1rem; 563 + color: var(--timestamp-color); 564 + text-align: center; 565 + margin: 0 0 1.5rem 0; 566 + } 567 + 568 + .wrappedGrid { 569 + display: grid; 570 + grid-template-columns: repeat(4, 1fr); 571 + gap: 1rem; 572 + margin-bottom: 1.5rem; 573 + } 574 + 575 + .wrappedCard { 576 + background-color: var(--card-background); 577 + border-radius: 8px; 578 + padding: 1.25rem 1rem; 579 + text-align: center; 580 + border: 1px solid var(--tile-border); 581 + transition: all 0.2s; 582 + } 583 + 584 + .wrappedCard:hover { 585 + border-color: var(--primary-color); 586 + } 587 + 588 + .wrappedValue { 589 + font-size: 1.8rem; 590 + font-weight: 700; 591 + color: var(--primary-color); 592 + margin-bottom: 0.5rem; 593 + line-height: 1.2; 594 + word-wrap: break-word; 595 + } 596 + 597 + .wrappedEmoji { 598 + font-size: 2.5rem; 599 + margin-bottom: 0.25rem; 600 + } 601 + 602 + .wrappedLabel { 603 + font-size: 0.9rem; 604 + color: var(--text-color); 605 + font-weight: 500; 606 + line-height: 1.3; 607 + } 608 + 609 + .shareWrappedButton { 610 + display: block; 611 + width: 100%; 612 + background-color: var(--primary-color); 613 + color: white; 614 + border: none; 615 + border-radius: 4px; 616 + padding: 1rem 1.5rem; 617 + font-size: 1.1rem; 618 + font-weight: 600; 619 + cursor: pointer; 620 + transition: all 0.2s; 621 + } 622 + 623 + .shareWrappedButton:hover { 624 + background-color: var(--secondary-color); 625 + } 626 + 627 + @media (max-width: 768px) { 628 + .wrappedGrid { 629 + grid-template-columns: repeat(2, 1fr); 630 + } 631 + 632 + .wrappedHeader { 633 + font-size: 1.5rem; 634 + } 635 + 636 + .wrappedValue { 637 + font-size: 1.5rem; 638 + } 639 + 640 + .wrappedEmoji { 641 + font-size: 2rem; 642 + } 643 + } 644 + 645 + @media (max-width: 480px) { 646 + .wrappedSection { 647 + padding: 1.5rem; 648 + } 649 + 650 + .wrappedGrid { 651 + grid-template-columns: 1fr; 652 + gap: 0.75rem; 653 + } 654 + 655 + .wrappedCard { 656 + padding: 1rem 0.75rem; 657 + } 543 658 }
-1
src/app/shortcut/shortcut.module.css
··· 30 30 background-color: var(--card-background); 31 31 border-radius: 1rem; 32 32 overflow: hidden; 33 - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); 34 33 margin-bottom: 3rem; 35 34 border: 1px solid var(--tile-border); 36 35 }
+11 -11
src/app/stats/page.tsx
··· 136 136 {/* Overall Stats */} 137 137 <section className={styles.overallStats}> 138 138 <h2>Overall Flush Activity</h2> 139 - <a 140 - href="https://bsky.app/profile/plumber.flushes.app" 141 - target="_blank" 142 - rel="noopener noreferrer" 143 - className={styles.plumberProfileLink} 144 - > 145 - Follow our resident plumber on Bluesky 146 - </a> 147 139 <div className={styles.statsGrid}> 148 140 <div className={styles.statCard}> 149 141 <div className={styles.statValue}>{statsData.totalCount}</div> ··· 174 166 175 167 {/* Activity Chart */} 176 168 <section className={styles.chartSection}> 177 - <h2>Daily Activity</h2> 169 + <h2>Monthly Activity</h2> 178 170 {statsData.chartData.length > 0 ? ( 179 171 <> 180 172 <div className={styles.chartContainer}> ··· 196 188 197 189 <div className={styles.chartLegend}> 198 190 <span className={styles.chartLegendItem}> 199 - {statsData.chartData.length > 0 ? new Date(statsData.chartData[0].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''} 191 + {statsData.chartData.length > 0 ? (() => { 192 + const [year, month] = statsData.chartData[0].date.split('-'); 193 + const date = new Date(parseInt(year), parseInt(month) - 1); 194 + return date.toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); 195 + })() : ''} 200 196 </span> 201 197 <span className={styles.chartLegendItem}> 202 - {statsData.chartData.length > 0 ? new Date(statsData.chartData[statsData.chartData.length - 1].date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''} 198 + {statsData.chartData.length > 0 ? (() => { 199 + const [year, month] = statsData.chartData[statsData.chartData.length - 1].date.split('-'); 200 + const date = new Date(parseInt(year), parseInt(month) - 1); 201 + return date.toLocaleDateString(undefined, { month: 'short', year: 'numeric' }); 202 + })() : ''} 203 203 </span> 204 204 </div> 205 205 </>
-1
src/app/stats/stats.module.css
··· 223 223 background: var(--card-background); 224 224 border-radius: 8px; 225 225 padding: 1.5rem; 226 - box-shadow: 0 2px 8px var(--shadow-color); 227 226 border: 1px solid var(--tile-border); 228 227 } 229 228
-2
src/components/ProfileSearch.module.css
··· 17 17 18 18 .searchForm:focus-within { 19 19 border-color: var(--primary-color); 20 - box-shadow: 0 0 0 2px rgba(91, 173, 240, 0.25); 21 20 } 22 21 23 22 .searchInput { ··· 61 60 background-color: var(--card-background); 62 61 border: 1px solid var(--tile-border); 63 62 border-radius: 8px; 64 - box-shadow: 0 4px 12px var(--shadow-color); 65 63 max-height: 300px; 66 64 overflow-y: auto; 67 65 z-index: 10;
-1
src/components/ThemeToggle.module.css
··· 17 17 .themeToggle:hover { 18 18 background-color: var(--button-hover); 19 19 transform: translateY(-2px); 20 - box-shadow: 0 2px 4px var(--shadow-color); 21 20 } 22 21 23 22 .themeToggle svg {