This repository has no description
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}