This repository has no description
0

Configure Feed

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

add emoji usage to profiles

+206 -13
+106 -3
app/src/app/api/bluesky/profile/route.ts
··· 16 16 created_at: string; 17 17 } 18 18 19 + // Define type for emoji statistics 20 + interface EmojiStat { 21 + emoji: string; 22 + count: number; 23 + } 24 + 25 + // Define interface for profile response 26 + interface ProfileResponse { 27 + entries: ProfileEntry[]; 28 + count: number; 29 + cursor?: string; 30 + profile?: any; 31 + emojiStats?: EmojiStat[]; 32 + } 33 + 19 34 const DEFAULT_API_URL = 'https://bsky.social/xrpc'; 20 35 const MAX_ENTRIES = 50; 36 + 37 + // Define approved emojis list - keep in sync with stats route 38 + const APPROVED_EMOJIS = [ 39 + '🚽', '🧻', '💩', '💨', '🚾', '🧼', '🪠', '🚻', '🩸', '💧', '💦', '😌', 40 + '😣', '🤢', '🤮', '🥴', '😮‍💨', '😳', '😵', '🌾', '🍦', '📱', '📖', '💭', 41 + '1️⃣', '2️⃣', '🟡', '🟤' 42 + ]; 21 43 const FLUSHING_STATUS_NSID = 'im.flushing.right.now'; 22 44 23 45 // Supabase client - using environment variables ··· 161 183 }) 162 184 .filter((entry: ProfileEntry | null): entry is ProfileEntry => entry !== null); // Remove filtered entries 163 185 186 + // Calculate emoji statistics 187 + const emojiCounts = new Map<string, number>(); 188 + 189 + // Process entries to count emojis 190 + transformedEntries.forEach(entry => { 191 + if (entry.emoji) { 192 + // Default to toilet emoji if empty 193 + const emoji = entry.emoji.trim() || '🚽'; 194 + // Only count approved emojis 195 + if (APPROVED_EMOJIS.includes(emoji)) { 196 + emojiCounts.set(emoji, (emojiCounts.get(emoji) || 0) + 1); 197 + } else { 198 + // Count as default toilet emoji if not approved 199 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 200 + } 201 + } else { 202 + // Count default toilet emoji if no emoji specified 203 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 204 + } 205 + }); 206 + 207 + // Convert to array and sort by count (most popular first) 208 + const emojiStats = Array.from(emojiCounts.entries()) 209 + .map(([emoji, count]): EmojiStat => ({ emoji, count })) 210 + .sort((a, b) => b.count - a.count); 211 + 164 212 return NextResponse.json({ 165 213 entries: transformedEntries, 166 214 count: transformedEntries.length, 167 215 cursor: fallbackData.cursor, 168 - profile: userProfile 216 + profile: userProfile, 217 + emojiStats 169 218 }); 170 219 } 171 220 ··· 199 248 }) 200 249 .filter((entry: ProfileEntry | null): entry is ProfileEntry => entry !== null); // Remove filtered entries 201 250 251 + // Calculate emoji statistics 252 + const emojiCounts = new Map<string, number>(); 253 + 254 + // Process entries to count emojis 255 + transformedEntries.forEach(entry => { 256 + if (entry.emoji) { 257 + // Default to toilet emoji if empty 258 + const emoji = entry.emoji.trim() || '🚽'; 259 + // Only count approved emojis 260 + if (APPROVED_EMOJIS.includes(emoji)) { 261 + emojiCounts.set(emoji, (emojiCounts.get(emoji) || 0) + 1); 262 + } else { 263 + // Count as default toilet emoji if not approved 264 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 265 + } 266 + } else { 267 + // Count default toilet emoji if no emoji specified 268 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 269 + } 270 + }); 271 + 272 + // Convert to array and sort by count (most popular first) 273 + const emojiStats = Array.from(emojiCounts.entries()) 274 + .map(([emoji, count]): EmojiStat => ({ emoji, count })) 275 + .sort((a, b) => b.count - a.count); 276 + 202 277 return NextResponse.json({ 203 278 entries: transformedEntries, 204 279 count: transformedEntries.length, 205 280 cursor: recordsData.cursor, 206 - profile: userProfile 281 + profile: userProfile, 282 + emojiStats 207 283 }); 208 284 } catch (error: any) { 209 285 console.error('Error fetching records:', error); ··· 243 319 }) 244 320 .filter((entry: any): entry is any => entry !== null); 245 321 322 + // Calculate emoji statistics for Supabase fallback entries 323 + const emojiCounts = new Map<string, number>(); 324 + 325 + // Process entries to count emojis 326 + filteredEntries.forEach((entry: any) => { 327 + if (entry.emoji) { 328 + // Default to toilet emoji if empty 329 + const emoji = entry.emoji.trim() || '🚽'; 330 + // Only count approved emojis 331 + if (APPROVED_EMOJIS.includes(emoji)) { 332 + emojiCounts.set(emoji, (emojiCounts.get(emoji) || 0) + 1); 333 + } else { 334 + // Count as default toilet emoji if not approved 335 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 336 + } 337 + } else { 338 + // Count default toilet emoji if no emoji specified 339 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 340 + } 341 + }); 342 + 343 + // Convert to array and sort by count (most popular first) 344 + const emojiStats = Array.from(emojiCounts.entries()) 345 + .map(([emoji, count]): EmojiStat => ({ emoji, count })) 346 + .sort((a, b) => b.count - a.count); 347 + 246 348 return NextResponse.json({ 247 349 entries: filteredEntries, 248 350 count: filteredEntries.length, // Update count to reflect filtered entries 249 - profile: userProfile 351 + profile: userProfile, 352 + emojiStats 250 353 }); 251 354 } 252 355
+16 -5
app/src/app/api/bluesky/stats/route.ts
··· 34 34 count: number; 35 35 }; 36 36 37 + // Define approved emojis list 38 + const APPROVED_EMOJIS = [ 39 + '🚽', '🧻', '💩', '💨', '🚾', '🧼', '🪠', '🚻', '🩸', '💧', '💦', '😌', 40 + '😣', '🤢', '🤮', '🥴', '😮‍💨', '😳', '😵', '🌾', '🍦', '📱', '📖', '💭', 41 + '1️⃣', '2️⃣', '🟡', '🟤' 42 + ]; 43 + 37 44 // If we have Supabase credentials, fetch stats 38 45 if (supabaseUrl && supabaseKey) { 39 46 const supabase = createClient(supabaseUrl, supabaseKey); ··· 280 287 if (entry.emoji) { 281 288 // Default to toilet emoji if empty 282 289 const emoji = entry.emoji.trim() || '🚽'; 283 - emojiCounts.set(emoji, (emojiCounts.get(emoji) || 0) + 1); 290 + // Only count approved emojis 291 + if (APPROVED_EMOJIS.includes(emoji)) { 292 + emojiCounts.set(emoji, (emojiCounts.get(emoji) || 0) + 1); 293 + } else { 294 + // Count as default toilet emoji if not approved 295 + emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); 296 + } 284 297 } else { 285 298 // Count default toilet emoji if no emoji specified 286 299 emojiCounts.set('🚽', (emojiCounts.get('🚽') || 0) + 1); ··· 371 384 372 385 // Generate mock emoji stats 373 386 function generateMockEmojiStats() { 374 - const popularEmojis = [ 375 - '🚽', '💩', '🧻', '💦', '🧼', '🪠', '🚿', '🛁', '🧴', '🌊', 376 - '💨', '🔥', '🚫', '⚠️', '🚪', '🧫', '📱', '🎮', '📖', '😌' 377 - ]; 387 + // Use the first 20 approved emojis for mock data 388 + const popularEmojis = APPROVED_EMOJIS.slice(0, 20); 378 389 379 390 return popularEmojis.map((emoji, index) => { 380 391 // Generate counts with descending values
+23
app/src/app/profile/[handle]/page.tsx
··· 18 18 created_at: string; 19 19 } 20 20 21 + // Interface for emoji statistics 22 + interface EmojiStat { 23 + emoji: string; 24 + count: number; 25 + } 26 + 21 27 export default function ProfilePage() { 22 28 const params = useParams(); 23 29 const handle = params.handle as string; ··· 30 36 const [profileError, setProfileError] = useState<string | null>(null); 31 37 const [flushesPerDay, setFlushesPerDay] = useState<number>(0); 32 38 const [chartData, setChartData] = useState<{date: string, count: number}[]>([]); 39 + const [emojiStats, setEmojiStats] = useState<EmojiStat[]>([]); 33 40 // Match Bluesky's API response format 34 41 interface ProfileData { 35 42 did: string; ··· 106 113 const userEntries = data.entries || []; 107 114 setEntries(userEntries); 108 115 setTotalCount(data.count || 0); 116 + setEmojiStats(data.emojiStats || []); 109 117 110 118 // We now fetch profile data separately 111 119 ··· 255 263 </> 256 264 ) : ( 257 265 <p className={styles.noDataMessage}>Not enough data to display activity chart</p> 266 + )} 267 + 268 + {/* Emoji Statistics Section */} 269 + {emojiStats.length > 0 && ( 270 + <div className={styles.emojiStatsSection}> 271 + <h4 className={styles.emojiStatsHeader}>Favorite Emoji</h4> 272 + <div className={styles.emojiGrid}> 273 + {emojiStats.slice(0, 8).map((stat, index) => ( 274 + <div key={index} className={styles.emojiCard}> 275 + <div className={styles.emojiDisplay}>{stat.emoji}</div> 276 + <div className={styles.emojiCount}>{stat.count}</div> 277 + </div> 278 + ))} 279 + </div> 280 + </div> 258 281 )} 259 282 </section> 260 283 )}
+56
app/src/app/profile/[handle]/profile.module.css
··· 177 177 font-style: italic; 178 178 } 179 179 180 + /* Emoji Statistics Section */ 181 + .emojiStatsSection { 182 + margin-top: 2rem; 183 + padding-top: 1.5rem; 184 + border-top: 1px solid var(--tile-border); 185 + } 186 + 187 + .emojiStatsHeader { 188 + font-size: 1.1rem; 189 + font-weight: 500; 190 + margin: 0 0 1rem 0; 191 + color: var(--title-color); 192 + } 193 + 194 + .emojiGrid { 195 + display: grid; 196 + grid-template-columns: repeat(4, 1fr); 197 + gap: 0.75rem; 198 + } 199 + 200 + .emojiCard { 201 + display: flex; 202 + flex-direction: column; 203 + align-items: center; 204 + background-color: var(--input-background); 205 + border-radius: 8px; 206 + padding: 0.75rem 0.5rem; 207 + border: 1px solid var(--tile-border); 208 + } 209 + 210 + .emojiDisplay { 211 + font-size: 1.5rem; 212 + margin-bottom: 0.5rem; 213 + } 214 + 215 + .emojiCount { 216 + font-weight: bold; 217 + color: var(--primary-color); 218 + font-size: 0.9rem; 219 + } 220 + 221 + @media (max-width: 600px) { 222 + .emojiGrid { 223 + grid-template-columns: repeat(3, 1fr); 224 + gap: 0.5rem; 225 + } 226 + 227 + .emojiDisplay { 228 + font-size: 1.3rem; 229 + } 230 + 231 + .emojiCount { 232 + font-size: 0.85rem; 233 + } 234 + } 235 + 180 236 .headerActions { 181 237 display: flex; 182 238 gap: 1rem;
+3 -3
app/src/app/stats/stats.module.css
··· 413 413 /* Emoji Grid */ 414 414 .emojiGrid { 415 415 display: grid; 416 - grid-template-columns: repeat(5, 1fr); 416 + grid-template-columns: repeat(7, 1fr); 417 417 gap: 1rem; 418 418 margin-top: 1.5rem; 419 419 } ··· 431 431 } 432 432 433 433 .emojiCard .emoji { 434 - font-size: 2rem; 434 + font-size: 1.5rem; 435 435 margin-bottom: 0.5rem; 436 436 } 437 437 438 438 .emojiCard .emojiCount { 439 439 font-weight: bold; 440 440 color: var(--primary-color); 441 - font-size: 1.2rem; 441 + font-size: 1rem; 442 442 } 443 443 444 444 /* Share Button */
+2 -2
app/src/components/ProfileSearch.tsx
··· 26 26 useEffect(() => { 27 27 const updatePlaceholder = () => { 28 28 if (window.innerWidth <= 480) { 29 - setPlaceholder('@handle or DID'); 29 + setPlaceholder('Search handle or DID'); 30 30 } else { 31 - setPlaceholder('Search user @handle or did:plc:...'); 31 + setPlaceholder('Search handle or DID'); 32 32 } 33 33 }; 34 34