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