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 { useRouter } from 'next/navigation'; 5import { useAuth } from '@/lib/auth-context'; 6import styles from './dashboard.module.css'; 7import Link from 'next/link'; 8 9// List of relevant emojis for flushing situations 10const EMOJIS = [ 11 '🚽', '🧻', '💩', '💨', '🚾', '🧼', '🪠', '🚻', '🩸', '💧', '💦', '😌', 12 '😣', '🤢', '🤮', '🥴', '😮‍💨', '😳', '😵', '🌾', '🍦', '📱', '📖', '💭', 13 '1️⃣', '2️⃣', '🟡', '🟤' 14]; 15 16// Types for our feed entries 17interface FlushingEntry { 18 id: string; 19 uri: string; 20 cid: string; 21 authorDid: string; 22 authorHandle: string; 23 text: string; 24 emoji: string; 25 createdAt: string; 26} 27 28export default function DashboardPage() { 29 const router = useRouter(); 30 const { isAuthenticated, session, signOut } = useAuth(); 31 const did = session?.sub; 32 const handle = null; // Will be fetched when needed 33 34 const [text, setText] = useState(''); 35 const [selectedEmoji, setSelectedEmoji] = useState(EMOJIS[0]); 36 const [isSubmitting, setIsSubmitting] = useState(false); 37 const [error, setError] = useState<string | null>(null); 38 const [success, setSuccess] = useState<string | null>(null); 39 const [statusOpen, setStatusOpen] = useState(false); 40 41 // Feed state 42 const [entries, setEntries] = useState<FlushingEntry[]>([]); 43 const [loadingFeed, setLoadingFeed] = useState(true); 44 const [feedError, setFeedError] = useState<string | null>(null); 45 const [newEntryIds, setNewEntryIds] = useState<Set<string>>(new Set()); 46 47 useEffect(() => { 48 // Redirect to home if not authenticated 49 if (!isAuthenticated) { 50 router.push('/'); 51 } else { 52 // Fetch feed when component mounts 53 fetchLatestEntries(); 54 } 55 }, [isAuthenticated, router]); 56 57 // Function to fetch the latest entries 58 const fetchLatestEntries = async (forceRefresh = false) => { 59 try { 60 setLoadingFeed(true); 61 setFeedError(null); 62 63 // Call our API endpoint to get the latest entries 64 // Add refresh parameter to bypass cache if needed 65 const url = forceRefresh 66 ? '/api/bluesky/feed?refresh=true' 67 : '/api/bluesky/feed'; 68 69 const response = await fetch(url, { 70 // Prevent browser caching 71 cache: 'no-store', 72 headers: { 73 'Cache-Control': 'no-cache', 74 'Pragma': 'no-cache' 75 } 76 }); 77 78 if (!response.ok) { 79 throw new Error(`Failed to fetch feed: ${response.status}`); 80 } 81 82 const data = await response.json(); 83 84 // Check for new entries 85 if (entries.length > 0) { 86 const currentIds = new Set(entries.map((entry: FlushingEntry) => entry.id)); 87 const newEntries = data.entries.filter((entry: FlushingEntry) => !currentIds.has(entry.id)); 88 89 // Mark new entries for animation 90 if (newEntries.length > 0) { 91 setNewEntryIds(new Set(newEntries.map((entry: FlushingEntry) => entry.id))); 92 93 // Clear the animation markers after animation completes 94 setTimeout(() => { 95 setNewEntryIds(new Set()); 96 }, 2000); 97 } 98 } 99 100 setEntries(data.entries); 101 } catch (err: any) { 102 console.error('Error fetching feed:', err); 103 setFeedError(err.message || 'Failed to load feed'); 104 } finally { 105 setLoadingFeed(false); 106 } 107 }; 108 109 // Logout handler 110 const handleLogout = async () => { 111 await signOut(); 112 router.push('/'); 113 }; 114 115 // Toggle status update form 116 const toggleStatusUpdate = () => { 117 setStatusOpen(!statusOpen); 118 setError(null); 119 setSuccess(null); 120 }; 121 122 // Handle emoji selection 123 const handleEmojiSelect = (emoji: string) => { 124 setSelectedEmoji(emoji); 125 }; 126 127 // Submit flushing status 128 const handleSubmit = async (e: React.FormEvent) => { 129 e.preventDefault(); 130 131 if (!session || !isAuthenticated) { 132 setError('Please sign in to post a flush'); 133 return; 134 } 135 136 // Check character limit - 59 characters maximum (including "is " prefix) 137 const fullText = `is ${text || 'flushing'}`; 138 if (fullText.length > 59) { 139 setError("Your flush status is too long! Please keep it under 59 characters."); 140 return; 141 } 142 143 setIsSubmitting(true); 144 setError(null); 145 setSuccess(null); 146 147 try { 148 // Use the new simplified API client 149 const { createPost } = await import('@/lib/api-client'); 150 151 // Create the status update with the simplified API 152 // Just send the text with emoji - no need to add handle/name since OAuth session handles user identity 153 const result = await createPost(session, { 154 text: `is ${text || 'flushing'} ${selectedEmoji}`, 155 langs: ['en'] 156 }); 157 158 console.log('Status update result:', result); 159 160 // Reset form and show success message 161 setText(''); 162 setSuccess('Your flushing status has been updated!'); 163 164 // Close status form after successful submission 165 setTimeout(() => { 166 setStatusOpen(false); 167 }, 2000); 168 169 // Refresh the feed to show the new status 170 setTimeout(() => { 171 fetchLatestEntries(true); 172 }, 1000); 173 } catch (err: any) { 174 console.error('Failed to update status:', err); 175 setError(`Failed to update status: ${err.message || 'Unknown error'}`); 176 } finally { 177 setIsSubmitting(false); 178 } 179 }; 180 181 if (!isAuthenticated) { 182 return null; // Will redirect in useEffect 183 } 184 185 return ( 186 <div className={styles.container}> 187 <header className={styles.header}> 188 <h1>I&apos;m Flushing</h1> 189 <div className={styles.userInfo}> 190 <span>Logged in as: @{handle}</span> 191 <div className={styles.actions}> 192 <button 193 onClick={() => fetchLatestEntries(true)} 194 className={styles.feedButton} 195 > 196 Refresh Feed 197 </button> 198 <button onClick={handleLogout} className={styles.logoutButton}> 199 Logout 200 </button> 201 </div> 202 </div> 203 </header> 204 205 {/* Status update toggle button */} 206 <button 207 className={`${styles.toggleButton} ${statusOpen ? styles.toggleButtonActive : ''}`} 208 onClick={toggleStatusUpdate} 209 > 210 {statusOpen ? 'Close' : 'Update Your Status'} 211 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 212 <path d="M19 9L12 16L5 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> 213 </svg> 214 </button> 215 216 {/* Collapsible status update form */} 217 <div className={`${styles.statusUpdateContainer} ${statusOpen ? styles.statusUpdateOpen : ''}`}> 218 <div className={styles.card}> 219 {error && <div className={styles.error}>{error}</div>} 220 {success && <div className={styles.success}>{success}</div>} 221 222 <form onSubmit={handleSubmit} className={styles.form}> 223 <div className={styles.formGroup}> 224 <label>Select an emoji for your status</label> 225 <div className={styles.emojiGrid}> 226 {EMOJIS.map((emoji) => ( 227 <button 228 key={emoji} 229 type="button" 230 className={`${styles.emojiButton} ${ 231 emoji === selectedEmoji ? styles.selectedEmoji : '' 232 }`} 233 onClick={() => handleEmojiSelect(emoji)} 234 disabled={isSubmitting} 235 > 236 {emoji} 237 </button> 238 ))} 239 </div> 240 </div> 241 242 <div className={styles.formGroup}> 243 <label htmlFor="status">What&apos;s your status? (optional)</label> 244 <input 245 type="text" 246 id="status" 247 value={text} 248 onChange={(e) => setText(e.target.value)} 249 placeholder="What's happening in the bathroom... (optional)" 250 maxLength={60} 251 className={styles.input} 252 disabled={isSubmitting} 253 /> 254 <div className={styles.charCount}> 255 {text.length}/60 256 </div> 257 </div> 258 259 <div className={styles.preview}> 260 <div className={styles.previewTitle}>Preview:</div> 261 <div className={styles.previewContent}> 262 <span className={styles.previewEmoji}>{selectedEmoji}</span> 263 <span>{text || 'is flushing'}</span> 264 </div> 265 </div> 266 267 <button 268 type="submit" 269 className={styles.submitButton} 270 disabled={isSubmitting || `is ${text || 'flushing'}`.length > 59} 271 > 272 {isSubmitting ? 'Updating...' : 'Update Status'} 273 </button> 274 </form> 275 </div> 276 </div> 277 278 {/* Feed Section */} 279 <div className={styles.feedSection}> 280 <div className={styles.feedTitle}> 281 <h2>Recent Bathroom Updates</h2> 282 <button 283 onClick={() => fetchLatestEntries(true)} 284 disabled={loadingFeed} 285 > 286 {loadingFeed ? 'Loading...' : 'Refresh'} 287 </button> 288 </div> 289 290 {feedError && <div className={styles.error}>{feedError}</div>} 291 292 {loadingFeed ? ( 293 <div className={styles.loadingContainer}> 294 <div className={styles.loader}></div> 295 <span>Loading feed...</span> 296 </div> 297 ) : ( 298 <div className={styles.feedList}> 299 {entries.length > 0 ? ( 300 entries.map((entry) => ( 301 <div 302 key={entry.id} 303 className={`${styles.feedItem} ${newEntryIds.has(entry.id) ? styles.newFeedItem : ''}`} 304 > 305 <div className={styles.content}> 306 <div className={styles.contentLeft}> 307 <span className={styles.emoji}>{entry.emoji}</span> 308 <a 309 href={`https://bsky.app/profile/${entry.authorHandle}`} 310 target="_blank" 311 rel="noopener noreferrer" 312 className={styles.authorLink} 313 > 314 @{entry.authorHandle} 315 </a> 316 <span className={styles.text}> 317 {entry.text ? 318 (entry.text.length > 60 ? `${entry.text.substring(0, 60)}...` : entry.text) : 319 'is flushing'} 320 </span> 321 </div> 322 <span className={styles.timestamp}> 323 {new Date(entry.createdAt).toLocaleString()} 324 </span> 325 </div> 326 </div> 327 )) 328 ) : ( 329 <div className={styles.emptyState}> 330 <p>No entries found. Be the first to share your status!</p> 331 </div> 332 )} 333 </div> 334 )} 335 </div> 336 </div> 337 ); 338}