This repository has no description
0

Configure Feed

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

at main 11 kB View raw
1'use client'; 2 3import { useState, useEffect } from 'react'; 4import Link from 'next/link'; 5import styles from './stats.module.css'; 6import { formatRelativeTime } from '@/lib/time-utils'; 7import { useAuth } from '@/lib/auth-context'; 8 9interface StatsData { 10 totalCount: number; 11 flushesPerDay: number; 12 chartData: { date: string; count: number }[]; 13 leaderboard: { did: string; count: number; handle?: string; supabaseCount?: number }[]; 14 plumberFlushCount: number; 15 totalFlushers: number; 16 monthlyActiveFlushers: number; 17 dailyActiveFlushers: number; 18 emojiStats: { emoji: string; count: number }[]; 19} 20 21export default function StatsPage() { 22 const { isAuthenticated, session, signOut } = useAuth(); 23 const handle = null; // Will be fetched when needed 24 const [statsData, setStatsData] = useState<StatsData | null>(null); 25 const [loading, setLoading] = useState(true); 26 const [error, setError] = useState<string | null>(null); 27 28 // Function to handle logout 29 const handleLogout = async () => { 30 await signOut(); 31 }; 32 33 useEffect(() => { 34 // Fetch stats data when the component mounts 35 fetchStatsData(); 36 }, []); 37 38 // Function to fetch stats data 39 const fetchStatsData = async () => { 40 try { 41 setLoading(true); 42 setError(null); 43 44 // Add a timestamp to ensure we bypass any browser caching 45 const timestamp = Date.now(); 46 const url = `/api/bluesky/stats?_t=${timestamp}`; 47 48 console.log(`Fetching stats from ${url}`); 49 50 const response = await fetch(url, { 51 method: 'GET', 52 cache: 'no-store', 53 headers: { 54 'Cache-Control': 'no-cache, no-store, must-revalidate', 55 'Pragma': 'no-cache', 56 'Expires': '0' 57 } 58 }); 59 60 if (!response.ok) { 61 throw new Error(`Failed to fetch stats: ${response.status}`); 62 } 63 64 const data = await response.json(); 65 66 // Process leaderboard data - resolve handles when possible 67 const leaderboardWithHandles = await Promise.all( 68 data.leaderboard.map(async (item: { did: string; count: number }) => { 69 // Try to resolve the DID to a handle 70 try { 71 const handleResponse = await fetch(`https://plc.directory/${item.did}/data`); 72 if (handleResponse.ok) { 73 const didDoc = await handleResponse.json(); 74 // Extract handle from alsoKnownAs 75 const handleUrl = didDoc.alsoKnownAs?.[0]; 76 if (handleUrl && handleUrl.startsWith('at://')) { 77 const handle = handleUrl.substring(5); // Remove 'at://' 78 return { ...item, handle }; 79 } 80 } 81 } catch (e) { 82 console.error(`Failed to resolve handle for DID ${item.did}`, e); 83 } 84 // Return original item if handle resolution fails 85 return item; 86 }) 87 ); 88 89 setStatsData({ 90 ...data, 91 leaderboard: leaderboardWithHandles 92 }); 93 } catch (err: any) { 94 console.error('Error fetching stats:', err); 95 setError(err.message || 'Failed to load stats'); 96 } finally { 97 setLoading(false); 98 } 99 }; 100 101 return ( 102 <div className={styles.container}> 103 <div className={styles.statsHeader}> 104 <h2>Plumbing Stats 🪠</h2> 105 <p className={styles.statsSubtitle}> 106 Global statistics for the Flushes network 107 </p> 108 </div> 109 110 <div className={styles.controls}> 111 <button 112 onClick={() => fetchStatsData()} 113 className={styles.refreshButton} 114 disabled={loading} 115 > 116 {loading ? 'Loading...' : 'Refresh Stats'} 117 </button> 118 <Link href="/" className={styles.homeLink}> 119 Back to Feed 120 </Link> 121 </div> 122 123 {error && ( 124 <div className={styles.error}> 125 Error: {error} 126 </div> 127 )} 128 129 {loading ? ( 130 <div className={styles.loadingContainer}> 131 <div className={styles.loader}></div> 132 <p>Loading stats...</p> 133 </div> 134 ) : statsData ? ( 135 <div className={styles.statsContent}> 136 {/* Overall Stats */} 137 <section className={styles.overallStats}> 138 <h2>Overall Flush Activity</h2> 139 <div className={styles.statsGrid}> 140 <div className={styles.statCard}> 141 <div className={styles.statValue}>{statsData.totalCount}</div> 142 <div className={styles.statLabel}>Total flushes</div> 143 </div> 144 <div className={styles.statCard}> 145 <div className={styles.statValue}>{statsData.flushesPerDay}</div> 146 <div className={styles.statLabel}>Flushes per day</div> 147 </div> 148 <div className={styles.statCard}> 149 <div className={styles.statValue}>{statsData.plumberFlushCount}</div> 150 <div className={styles.statLabel}>Plumber test flushes</div> 151 </div> 152 <div className={styles.statCard}> 153 <div className={styles.statValue}>{statsData.totalFlushers}</div> 154 <div className={styles.statLabel}>Total flushers</div> 155 </div> 156 <div className={styles.statCard}> 157 <div className={styles.statValue}>{statsData.monthlyActiveFlushers}</div> 158 <div className={styles.statLabel}>Monthly active flushers</div> 159 </div> 160 <div className={styles.statCard}> 161 <div className={styles.statValue}>{statsData.dailyActiveFlushers}</div> 162 <div className={styles.statLabel}>Daily active flushers (avg)</div> 163 </div> 164 </div> 165 </section> 166 167 {/* Activity Chart */} 168 <section className={styles.chartSection}> 169 <h2>Monthly Activity</h2> 170 {statsData.chartData.length > 0 ? ( 171 <> 172 <div className={styles.chartContainer}> 173 {statsData.chartData.map((dataPoint, index) => { 174 // Calculate height percentage (max of 100%) 175 const maxCount = Math.max(...statsData.chartData.map(d => d.count)); 176 const heightPercent = dataPoint.count === 0 ? 0 : Math.min(100, (dataPoint.count / maxCount) * 100); 177 178 return ( 179 <div 180 key={index} 181 className={styles.chartBar} 182 style={{ height: `${heightPercent}%` }} 183 title={`${dataPoint.date}: ${dataPoint.count} flushes`} 184 /> 185 ); 186 })} 187 </div> 188 189 <div className={styles.chartLegend}> 190 <span className={styles.chartLegendItem}> 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 })() : ''} 196 </span> 197 <span className={styles.chartLegendItem}> 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 </span> 204 </div> 205 </> 206 ) : ( 207 <p className={styles.noDataMessage}>Not enough data to display activity chart</p> 208 )} 209 </section> 210 211 {/* Leaderboard */} 212 <section className={styles.leaderboardSection}> 213 <h2>Top Flushers</h2> 214 {statsData.leaderboard.length > 0 ? ( 215 <div className={styles.leaderboard}> 216 <div className={styles.leaderboardHeader}> 217 <span className={styles.rank}>Rank</span> 218 <span className={styles.user}>User</span> 219 <span className={styles.count}>Flushes</span> 220 </div> 221 {statsData.leaderboard.map((item, index) => { 222 // Determine rank style class based on position 223 let rankClass = ''; 224 if (index === 0) rankClass = styles.topRank; 225 else if (index === 1) rankClass = styles.secondRank; 226 else if (index === 2) rankClass = styles.thirdRank; 227 228 return ( 229 <div key={index} className={`${styles.leaderboardItem} ${rankClass}`}> 230 <span className={styles.rank}>#{index + 1}</span> 231 <span className={styles.user}> 232 {item.handle ? ( 233 <Link href={`/profile/${item.handle}`} title={`@${item.handle}`}> 234 @{item.handle} 235 </Link> 236 ) : ( 237 <span className={styles.unknownUser}> 238 {item.did.substring(0, 10)}... 239 </span> 240 )} 241 </span> 242 <span className={styles.count}>{item.count}</span> 243 </div> 244 ); 245 })} 246 </div> 247 ) : ( 248 <p className={styles.noDataMessage}>No leaderboard data available</p> 249 )} 250 </section> 251 252 {/* Emoji Statistics */} 253 <section className={styles.emojiSection}> 254 <h2>Emoji Usage</h2> 255 {statsData.emojiStats && statsData.emojiStats.length > 0 ? ( 256 <div className={styles.emojiGrid}> 257 {statsData.emojiStats.map((emojiStat, index) => ( 258 <div key={index} className={styles.emojiCard}> 259 <div className={styles.emoji}>{emojiStat.emoji}</div> 260 <div className={styles.emojiCount}>{emojiStat.count}</div> 261 </div> 262 ))} 263 </div> 264 ) : ( 265 <p className={styles.noDataMessage}>No emoji data available</p> 266 )} 267 </section> 268 269 <div className={styles.shareSection}> 270 <button 271 className={styles.shareButton} 272 onClick={() => { 273 // Generate share text 274 const statsText = `There have been ${statsData.totalCount} flushes by ${statsData.totalFlushers} unique users on @flushes.app! We have ${statsData.monthlyActiveFlushers} monthly active flushers and ${statsData.dailyActiveFlushers} daily active flushers on average. Check out the stats: https://flushes.app/stats`; 275 window.open(`https://bsky.app/intent/compose?text=${encodeURIComponent(statsText)}`, '_blank'); 276 }} 277 > 278 Share These Stats 279 </button> 280 </div> 281 </div> 282 ) : ( 283 <div className={styles.emptyState}> 284 <p>No stats data available</p> 285 </div> 286 )} 287 </div> 288 ); 289}