The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.

Implement Flush Record Management in Profile Page

- Added functionality to update and delete flush records, including user authentication checks.
- Introduced action messages for success and error feedback during flush operations.
- Enhanced the UI with an edit button for user-owned flush entries and a modal for editing flush details.
- Updated API client to support flush record deletion and updating.

dame-is dfd5e052 1332f0a8

+1000 -6
+218
EDIT_DELETE_FEATURE.md
··· 1 + # Edit & Delete Flush Feature 2 + 3 + ## Overview 4 + This feature allows users to edit and delete their own flush records directly from their profile page. The implementation includes a modal interface for editing, client-side validation, and proper handling through the AT Protocol and Jetstream firehose. 5 + 6 + ## What Was Added 7 + 8 + ### 1. API Client Functions (`src/lib/api-client.ts`) 9 + Added two new functions to handle record management: 10 + 11 + - **`deleteFlushRecord(session, recordUri)`**: Deletes a flush record using the AT Protocol's `deleteRecord` method 12 + - **`updateFlushRecord(session, recordUri, text, emoji, originalCreatedAt)`**: Updates a flush record using the AT Protocol's `putRecord` method 13 + 14 + Both functions: 15 + - Parse AT URIs correctly (format: `at://did:plc:xxx/collection.name/rkey`) 16 + - Use the OAuth session's Agent for authenticated requests 17 + - Include proper error handling and logging 18 + - Preserve the original `createdAt` timestamp on updates 19 + 20 + ### 2. Edit Modal Component (`src/components/EditFlushModal.tsx`) 21 + A beautiful modal dialog that provides: 22 + 23 + - Pre-populated form with the flush's current text and emoji 24 + - Character counter (59 character limit) 25 + - Emoji selector with all approved emojis 26 + - Content validation (banned words, character limits) 27 + - Delete confirmation workflow 28 + - Loading states for all async operations 29 + - Error handling with user-friendly messages 30 + - Backdrop click to close 31 + - Responsive design for mobile devices 32 + 33 + ### 3. Profile Page Updates (`src/app/profile/[handle]/page.tsx`) 34 + Enhanced the profile page with: 35 + 36 + - Edit button on each flush (only visible to the flush owner) 37 + - Authentication check using `useAuth()` hook to compare DIDs 38 + - Integration with `EditFlushModal` component 39 + - State management for editing operations 40 + - Success/error message display 41 + - Optimistic UI updates (updates local state immediately) 42 + 43 + New state variables: 44 + - `editingFlush`: Tracks which flush is being edited 45 + - `actionError`: Displays error messages 46 + - `actionSuccess`: Displays success messages 47 + 48 + New functions: 49 + - `isOwnProfile()`: Checks if the logged-in user owns the profile 50 + - `handleUpdateFlush()`: Handles the update operation 51 + - `handleDeleteFlush()`: Handles the delete operation 52 + 53 + ### 4. Profile Styles (`src/app/profile/[handle]/profile.module.css`) 54 + Added styles for: 55 + 56 + - `.contentRight`: Container for timestamp and edit button 57 + - `.editButton`: Pencil icon button with hover effects 58 + - `.actionError`: Error message styling 59 + - `.actionSuccess`: Success message styling 60 + 61 + ### 5. Edit Modal Styles (`src/components/EditFlushModal.module.css`) 62 + Complete styling for the modal including: 63 + 64 + - Dark backdrop overlay 65 + - Centered modal with max-width 66 + - Form inputs and emoji grid 67 + - Action buttons (Save, Cancel, Delete) 68 + - Delete confirmation UI 69 + - Responsive mobile layout 70 + - Smooth transitions and hover effects 71 + 72 + ### 6. Jetstream Consumer (`scripts/firehose-worker.js`) 73 + Updated to properly handle: 74 + 75 + - **Delete operations**: Removes records from Supabase when deleted from the network 76 + - **Update operations**: Updates existing records with new content 77 + - URI construction for record matching 78 + - Proper error handling for database operations 79 + 80 + ## How It Works 81 + 82 + ### User Flow 83 + 84 + 1. **User navigates to their own profile** 85 + - Edit buttons appear next to each of their flushes 86 + - Buttons are hidden for other users' profiles 87 + 88 + 2. **User clicks edit button** 89 + - Modal opens with pre-filled form 90 + - Current text and emoji are displayed 91 + - User can modify text and/or emoji 92 + - Character counter shows remaining characters 93 + 94 + 3. **User saves changes** 95 + - Validation runs (banned words, character limits) 96 + - API call made to update the record via AT Protocol 97 + - Local state updates immediately (optimistic UI) 98 + - Success message displayed 99 + - Modal closes automatically 100 + 101 + 4. **User deletes a flush** 102 + - Clicks "Delete Flush" button 103 + - Confirmation prompt appears 104 + - On confirmation, record is deleted via AT Protocol 105 + - Record removed from local state 106 + - Success message displayed 107 + - Modal closes 108 + 109 + ### Technical Flow 110 + 111 + #### Update Operation 112 + ``` 113 + User clicks Save 114 + → Validation (client-side) 115 + → updateFlushRecord(session, uri, text, emoji, createdAt) 116 + → Agent.api.com.atproto.repo.putRecord() 117 + → PDS updates the record 118 + → Jetstream firehose emits 'update' event 119 + → Worker processes event 120 + → Supabase record updated 121 + → UI updates optimistically 122 + ``` 123 + 124 + #### Delete Operation 125 + ``` 126 + User confirms delete 127 + → deleteFlushRecord(session, uri) 128 + → Agent.api.com.atproto.repo.deleteRecord() 129 + → PDS deletes the record 130 + → Jetstream firehose emits 'delete' event 131 + → Worker processes event 132 + → Supabase record deleted 133 + → UI updates optimistically 134 + ``` 135 + 136 + ## Authorization 137 + 138 + - Uses OAuth session from `@atproto/oauth-client-browser` 139 + - Compares `session.sub` (user's DID) with `profileData.did` 140 + - Edit buttons only visible when DIDs match 141 + - AT Protocol handles authorization at the PDS level 142 + - Users can only edit/delete their own records 143 + 144 + ## Validation 145 + 146 + All validation from the original flush creation is preserved: 147 + 148 + - **Character limit**: 59 characters 149 + - **Banned words**: Content filtering via `containsBannedWords()` 150 + - **Text sanitization**: via `sanitizeText()` 151 + - **Emoji validation**: Only approved emojis from the list 152 + - **Authentication**: Must be logged in 153 + 154 + ## Error Handling 155 + 156 + Comprehensive error handling at every level: 157 + 158 + - Network failures 159 + - Authorization errors 160 + - Validation errors 161 + - User-friendly error messages 162 + - Console logging for debugging 163 + - Graceful degradation 164 + 165 + ## Responsive Design 166 + 167 + The modal and edit buttons work beautifully on: 168 + 169 + - Desktop screens 170 + - Tablets 171 + - Mobile devices 172 + 173 + Features: 174 + - Touch-friendly button sizes 175 + - Readable text at all sizes 176 + - Scrollable modal content 177 + - Proper spacing and padding 178 + 179 + ## Future Enhancements 180 + 181 + Potential improvements: 182 + 183 + 1. **Edit history**: Track when records were edited 184 + 2. **Undo functionality**: Allow users to revert changes 185 + 3. **Bulk operations**: Edit/delete multiple flushes at once 186 + 4. **Keyboard shortcuts**: Quick access to edit/delete 187 + 5. **Inline editing**: Edit directly in the feed without modal 188 + 6. **Draft saving**: Save edits as drafts before publishing 189 + 190 + ## Testing Checklist 191 + 192 + To verify the feature works correctly: 193 + 194 + - [ ] Edit button appears on own profile only 195 + - [ ] Edit button hidden on other users' profiles 196 + - [ ] Modal opens when edit button clicked 197 + - [ ] Form pre-populates with current values 198 + - [ ] Text changes are validated 199 + - [ ] Emoji selection works 200 + - [ ] Character counter updates correctly 201 + - [ ] Save button updates the record 202 + - [ ] Delete button shows confirmation 203 + - [ ] Delete confirmation works 204 + - [ ] Cancel buttons close modal 205 + - [ ] Success messages display 206 + - [ ] Error messages display 207 + - [ ] Local state updates optimistically 208 + - [ ] Jetstream updates Supabase correctly 209 + - [ ] Mobile layout works properly 210 + 211 + ## Notes 212 + 213 + - Records maintain their original `createdAt` timestamp when updated 214 + - Updates create a new CID (Content Identifier) for the record 215 + - The URI remains the same (same `rkey`) 216 + - Deletes are permanent and cannot be undone 217 + - All operations respect AT Protocol's distributed architecture 218 +
+29 -3
scripts/firehose-worker.js
··· 342 342 343 343 console.log(`Processing ${operation} operation for DID: ${did}, collection: ${collection}, rkey: ${rkey}`); 344 344 345 - // Skip delete operations 345 + // Construct the URI for the record 346 + const recordUri = `at://${did}/${collection}/${rkey}`; 347 + 348 + // Handle delete operations 346 349 if (operation === 'delete') { 347 - console.log(`Skipping delete operation`); 348 - return; 350 + console.log(`Processing delete operation for URI: ${recordUri}`); 351 + 352 + try { 353 + const { data, error } = await supabase 354 + .from('flushing_records') 355 + .delete() 356 + .eq('uri', recordUri); 357 + 358 + if (error) { 359 + console.error(`Error deleting record: ${error.message}`); 360 + } else { 361 + console.log(`Successfully deleted record: ${recordUri}`); 362 + } 363 + } catch (deleteError) { 364 + console.error(`Exception while deleting record: ${deleteError.message}`); 365 + } 366 + 367 + return; // Done processing delete 368 + } 369 + 370 + // Handle update operations (which are represented as 'update' in Jetstream) 371 + if (operation === 'update') { 372 + console.log(`Processing update operation for URI: ${recordUri}`); 373 + // Updates are handled the same way as creates - we'll update the existing record 374 + // Fall through to the normal processing below 349 375 } 350 376 351 377 // Try different approaches to get a handle
+124 -3
src/app/profile/[handle]/page.tsx
··· 6 6 import styles from './profile.module.css'; 7 7 import { sanitizeText, containsBannedWords } from '@/lib/content-filter'; 8 8 import { formatRelativeTime } from '@/lib/time-utils'; 9 + import { useAuth } from '@/lib/auth-context'; 10 + import EditFlushModal from '@/components/EditFlushModal'; 9 11 10 12 // Define approved emojis list - keep in sync with API route 11 13 const APPROVED_EMOJIS = [ ··· 34 36 export default function ProfilePage() { 35 37 const params = useParams(); 36 38 const handle = params.handle as string; 39 + const { session, isAuthenticated } = useAuth(); 37 40 38 41 const [entries, setEntries] = useState<FlushingEntry[]>([]); 39 42 const [totalCount, setTotalCount] = useState<number>(0); ··· 55 58 avgStatusLength: number; 56 59 mostFrequentTime: string; 57 60 } | null>(null); 61 + const [editingFlush, setEditingFlush] = useState<FlushingEntry | null>(null); 62 + const [actionError, setActionError] = useState<string | null>(null); 63 + const [actionSuccess, setActionSuccess] = useState<string | null>(null); 58 64 // Match Bluesky's API response format 59 65 interface ProfileData { 60 66 did: string; ··· 431 437 } 432 438 }; 433 439 440 + // Check if the current user owns this profile 441 + const isOwnProfile = () => { 442 + if (!session || !profileData) return false; 443 + return session.sub === profileData.did; 444 + }; 445 + 446 + // Handle updating a flush 447 + const handleUpdateFlush = async (text: string, emoji: string) => { 448 + if (!session || !editingFlush) { 449 + setActionError('You must be logged in to update a flush'); 450 + return; 451 + } 452 + 453 + try { 454 + setActionError(null); 455 + setActionSuccess(null); 456 + 457 + const { updateFlushRecord } = await import('@/lib/api-client'); 458 + 459 + await updateFlushRecord( 460 + session, 461 + editingFlush.uri, 462 + text, 463 + emoji, 464 + editingFlush.created_at 465 + ); 466 + 467 + setActionSuccess('Flush updated successfully!'); 468 + 469 + // Update the local state 470 + setEntries(entries.map(entry => 471 + entry.uri === editingFlush.uri 472 + ? { ...entry, text, emoji } 473 + : entry 474 + )); 475 + 476 + // Clear success message after 3 seconds 477 + setTimeout(() => setActionSuccess(null), 3000); 478 + } catch (error: any) { 479 + console.error('Error updating flush:', error); 480 + setActionError(error.message || 'Failed to update flush'); 481 + } 482 + }; 483 + 484 + // Handle deleting a flush 485 + const handleDeleteFlush = async () => { 486 + if (!session || !editingFlush) { 487 + setActionError('You must be logged in to delete a flush'); 488 + return; 489 + } 490 + 491 + try { 492 + setActionError(null); 493 + setActionSuccess(null); 494 + 495 + const { deleteFlushRecord } = await import('@/lib/api-client'); 496 + 497 + await deleteFlushRecord(session, editingFlush.uri); 498 + 499 + setActionSuccess('Flush deleted successfully!'); 500 + 501 + // Remove from local state 502 + setEntries(entries.filter(entry => entry.uri !== editingFlush.uri)); 503 + setTotalCount(totalCount - 1); 504 + 505 + // Clear success message after 3 seconds 506 + setTimeout(() => setActionSuccess(null), 3000); 507 + } catch (error: any) { 508 + console.error('Error deleting flush:', error); 509 + setActionError(error.message || 'Failed to delete flush'); 510 + } 511 + }; 512 + 434 513 return ( 435 514 <div className={styles.container}> 515 + 516 + {/* Action messages */} 517 + {actionError && ( 518 + <div className={styles.actionError}> 519 + {actionError} 520 + </div> 521 + )} 522 + 523 + {actionSuccess && ( 524 + <div className={styles.actionSuccess}> 525 + {actionSuccess} 526 + </div> 527 + )} 528 + 529 + {/* Edit Modal */} 530 + <EditFlushModal 531 + isOpen={editingFlush !== null} 532 + flushData={editingFlush ? { 533 + uri: editingFlush.uri, 534 + text: editingFlush.text, 535 + emoji: editingFlush.emoji, 536 + created_at: editingFlush.created_at 537 + } : null} 538 + onSave={handleUpdateFlush} 539 + onDelete={handleDeleteFlush} 540 + onClose={() => setEditingFlush(null)} 541 + /> 436 542 437 543 <div className={styles.profileHeader}> 438 544 <div className={styles.profileInfo}> ··· 648 754 )} 649 755 </span> 650 756 </div> 651 - <span className={styles.timestamp}> 652 - {formatRelativeTime(entry.created_at)} 653 - </span> 757 + <div className={styles.contentRight}> 758 + <span className={styles.timestamp}> 759 + {formatRelativeTime(entry.created_at)} 760 + </span> 761 + {isOwnProfile() && isAuthenticated && ( 762 + <button 763 + className={styles.editButton} 764 + onClick={() => setEditingFlush(entry)} 765 + aria-label="Edit flush" 766 + title="Edit or delete this flush" 767 + > 768 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 769 + <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 770 + <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 771 + </svg> 772 + </button> 773 + )} 774 + </div> 654 775 </div> 655 776 </div> 656 777 ))
+49
src/app/profile/[handle]/profile.module.css
··· 446 446 min-width: 0; 447 447 } 448 448 449 + .contentRight { 450 + display: flex; 451 + align-items: center; 452 + gap: 0.75rem; 453 + } 454 + 455 + .editButton { 456 + background: none; 457 + border: 1px solid var(--border); 458 + color: var(--foreground); 459 + padding: 6px; 460 + cursor: pointer; 461 + display: flex; 462 + align-items: center; 463 + justify-content: center; 464 + transition: all 0.2s; 465 + width: 32px; 466 + height: 32px; 467 + } 468 + 469 + .editButton svg { 470 + width: 16px; 471 + height: 16px; 472 + } 473 + 474 + .editButton:hover { 475 + border-color: var(--primary-color); 476 + color: var(--primary-color); 477 + background: rgba(var(--primary-rgb), 0.05); 478 + } 479 + 480 + .actionError { 481 + background: #fee; 482 + border: 1px solid #fcc; 483 + color: #c33; 484 + padding: 12px; 485 + margin-bottom: 16px; 486 + font-size: 0.9rem; 487 + } 488 + 489 + .actionSuccess { 490 + background: #efe; 491 + border: 1px solid #cfc; 492 + color: #363; 493 + padding: 12px; 494 + margin-bottom: 16px; 495 + font-size: 0.9rem; 496 + } 497 + 449 498 .userLine { 450 499 display: flex; 451 500 align-items: center;
+280
src/components/EditFlushModal.module.css
··· 1 + .modalBackdrop { 2 + position: fixed; 3 + top: 0; 4 + left: 0; 5 + right: 0; 6 + bottom: 0; 7 + background: rgba(0, 0, 0, 0.7); 8 + display: flex; 9 + align-items: center; 10 + justify-content: center; 11 + z-index: 1000; 12 + padding: 20px; 13 + } 14 + 15 + .modalContent { 16 + background: var(--background); 17 + border: 2px solid var(--border); 18 + padding: 30px; 19 + max-width: 600px; 20 + width: 100%; 21 + max-height: 90vh; 22 + overflow-y: auto; 23 + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); 24 + } 25 + 26 + .modalHeader { 27 + display: flex; 28 + justify-content: space-between; 29 + align-items: center; 30 + margin-bottom: 20px; 31 + } 32 + 33 + .modalHeader h2 { 34 + margin: 0; 35 + font-size: 1.5rem; 36 + color: var(--foreground); 37 + } 38 + 39 + .closeButton { 40 + background: none; 41 + border: none; 42 + font-size: 1.5rem; 43 + cursor: pointer; 44 + color: var(--foreground); 45 + padding: 5px 10px; 46 + line-height: 1; 47 + transition: color 0.2s; 48 + } 49 + 50 + .closeButton:hover { 51 + color: var(--primary); 52 + } 53 + 54 + .closeButton:disabled { 55 + opacity: 0.5; 56 + cursor: not-allowed; 57 + } 58 + 59 + .error { 60 + background: #fee; 61 + border: 1px solid #fcc; 62 + color: #c33; 63 + padding: 12px; 64 + margin-bottom: 20px; 65 + font-size: 0.9rem; 66 + } 67 + 68 + .formGroup { 69 + margin-bottom: 24px; 70 + } 71 + 72 + .formGroup label { 73 + display: block; 74 + margin-bottom: 8px; 75 + font-weight: 600; 76 + color: var(--foreground); 77 + font-size: 0.95rem; 78 + } 79 + 80 + .textInput { 81 + width: 100%; 82 + padding: 12px; 83 + font-size: 1rem; 84 + border: 2px solid var(--border); 85 + background: var(--background); 86 + color: var(--foreground); 87 + font-family: inherit; 88 + transition: border-color 0.2s; 89 + } 90 + 91 + .textInput:focus { 92 + outline: none; 93 + border-color: var(--primary); 94 + } 95 + 96 + .textInput:disabled { 97 + opacity: 0.6; 98 + cursor: not-allowed; 99 + } 100 + 101 + .charCount { 102 + text-align: right; 103 + font-size: 0.85rem; 104 + color: var(--muted); 105 + margin-top: 4px; 106 + } 107 + 108 + .emojiGrid { 109 + display: grid; 110 + grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); 111 + gap: 8px; 112 + margin-top: 8px; 113 + } 114 + 115 + .emojiButton { 116 + background: var(--background); 117 + border: 2px solid var(--border); 118 + padding: 12px; 119 + font-size: 1.5rem; 120 + cursor: pointer; 121 + transition: all 0.2s; 122 + display: flex; 123 + align-items: center; 124 + justify-content: center; 125 + } 126 + 127 + .emojiButton:hover { 128 + border-color: var(--primary); 129 + transform: scale(1.05); 130 + } 131 + 132 + .emojiButton.selected { 133 + background: var(--primary); 134 + border-color: var(--primary); 135 + transform: scale(1.1); 136 + } 137 + 138 + .emojiButton:disabled { 139 + opacity: 0.5; 140 + cursor: not-allowed; 141 + } 142 + 143 + .modalActions { 144 + display: flex; 145 + justify-content: space-between; 146 + align-items: center; 147 + gap: 12px; 148 + margin-top: 24px; 149 + padding-top: 24px; 150 + border-top: 1px solid var(--border); 151 + } 152 + 153 + .rightActions { 154 + display: flex; 155 + gap: 12px; 156 + } 157 + 158 + .deleteButton { 159 + background: transparent; 160 + color: #c33; 161 + border: 2px solid #c33; 162 + padding: 10px 20px; 163 + font-size: 0.95rem; 164 + font-weight: 600; 165 + cursor: pointer; 166 + transition: all 0.2s; 167 + } 168 + 169 + .deleteButton:hover { 170 + background: #c33; 171 + color: white; 172 + } 173 + 174 + .deleteButton:disabled { 175 + opacity: 0.5; 176 + cursor: not-allowed; 177 + } 178 + 179 + .cancelButton { 180 + background: transparent; 181 + color: var(--foreground); 182 + border: 2px solid var(--border); 183 + padding: 10px 20px; 184 + font-size: 0.95rem; 185 + font-weight: 600; 186 + cursor: pointer; 187 + transition: all 0.2s; 188 + } 189 + 190 + .cancelButton:hover { 191 + background: var(--border); 192 + } 193 + 194 + .cancelButton:disabled { 195 + opacity: 0.5; 196 + cursor: not-allowed; 197 + } 198 + 199 + .saveButton { 200 + background: var(--primary); 201 + color: var(--primary-foreground); 202 + border: 2px solid var(--primary); 203 + padding: 10px 20px; 204 + font-size: 0.95rem; 205 + font-weight: 600; 206 + cursor: pointer; 207 + transition: all 0.2s; 208 + } 209 + 210 + .saveButton:hover { 211 + opacity: 0.9; 212 + transform: translateY(-1px); 213 + } 214 + 215 + .saveButton:disabled { 216 + opacity: 0.5; 217 + cursor: not-allowed; 218 + transform: none; 219 + } 220 + 221 + .deleteConfirmation { 222 + width: 100%; 223 + } 224 + 225 + .deleteConfirmation p { 226 + margin: 0 0 16px 0; 227 + color: var(--foreground); 228 + font-size: 0.95rem; 229 + } 230 + 231 + .confirmButtons { 232 + display: flex; 233 + gap: 12px; 234 + justify-content: flex-end; 235 + } 236 + 237 + .confirmDeleteButton { 238 + background: #c33; 239 + color: white; 240 + border: 2px solid #c33; 241 + padding: 10px 20px; 242 + font-size: 0.95rem; 243 + font-weight: 600; 244 + cursor: pointer; 245 + transition: all 0.2s; 246 + } 247 + 248 + .confirmDeleteButton:hover { 249 + background: #a22; 250 + border-color: #a22; 251 + } 252 + 253 + .confirmDeleteButton:disabled { 254 + opacity: 0.5; 255 + cursor: not-allowed; 256 + } 257 + 258 + @media (max-width: 640px) { 259 + .modalContent { 260 + padding: 20px; 261 + max-height: 95vh; 262 + } 263 + 264 + .modalActions { 265 + flex-direction: column; 266 + align-items: stretch; 267 + } 268 + 269 + .rightActions { 270 + flex-direction: column; 271 + } 272 + 273 + .deleteButton, 274 + .cancelButton, 275 + .saveButton, 276 + .confirmDeleteButton { 277 + width: 100%; 278 + } 279 + } 280 +
+208
src/components/EditFlushModal.tsx
··· 1 + 'use client'; 2 + 3 + import { useState, useEffect } from 'react'; 4 + import styles from './EditFlushModal.module.css'; 5 + import { containsBannedWords, sanitizeText, isAllowedEmoji } from '@/lib/content-filter'; 6 + 7 + interface EditFlushModalProps { 8 + isOpen: boolean; 9 + flushData: { 10 + uri: string; 11 + text: string; 12 + emoji: string; 13 + created_at: string; 14 + } | null; 15 + onSave: (text: string, emoji: string) => Promise<void>; 16 + onDelete: () => Promise<void>; 17 + onClose: () => void; 18 + } 19 + 20 + // Define approved emojis list 21 + const APPROVED_EMOJIS = [ 22 + '🚽', '🧻', '💩', '💨', '🚾', '🧼', '🪠', '🚻', '🩸', '💧', '💦', '😌', 23 + '😣', '🤢', '🤮', '🥴', '😮‍💨', '😳', '😵', '🌾', '🍦', '📱', '📖', '💭', 24 + '1️⃣', '2️⃣', '🟡', '🟤' 25 + ]; 26 + 27 + export default function EditFlushModal({ isOpen, flushData, onSave, onDelete, onClose }: EditFlushModalProps) { 28 + const [text, setText] = useState(''); 29 + const [selectedEmoji, setSelectedEmoji] = useState('🚽'); 30 + const [isSubmitting, setIsSubmitting] = useState(false); 31 + const [error, setError] = useState<string | null>(null); 32 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); 33 + 34 + // Update form when flushData changes 35 + useEffect(() => { 36 + if (flushData) { 37 + setText(flushData.text || ''); 38 + setSelectedEmoji(flushData.emoji || '🚽'); 39 + setError(null); 40 + setShowDeleteConfirm(false); 41 + } 42 + }, [flushData]); 43 + 44 + if (!isOpen || !flushData) return null; 45 + 46 + const handleSave = async () => { 47 + setError(null); 48 + 49 + // Validate text 50 + if (containsBannedWords(text)) { 51 + setError('Uh oh, looks like you have a potty mouth. Try again with cleaner language please...'); 52 + return; 53 + } 54 + 55 + // Check character limit 56 + if (text.length > 59) { 57 + setError('Your flush status is too long! Please keep it under 59 characters.'); 58 + return; 59 + } 60 + 61 + // Validate emoji 62 + if (!isAllowedEmoji(selectedEmoji)) { 63 + setError('Please select a valid emoji from the list.'); 64 + return; 65 + } 66 + 67 + setIsSubmitting(true); 68 + try { 69 + await onSave(sanitizeText(text), selectedEmoji); 70 + onClose(); 71 + } catch (err: any) { 72 + console.error('Error updating flush:', err); 73 + setError(err.message || 'Failed to update flush. Please try again.'); 74 + } finally { 75 + setIsSubmitting(false); 76 + } 77 + }; 78 + 79 + const handleDelete = async () => { 80 + setIsSubmitting(true); 81 + setError(null); 82 + try { 83 + await onDelete(); 84 + onClose(); 85 + } catch (err: any) { 86 + console.error('Error deleting flush:', err); 87 + setError(err.message || 'Failed to delete flush. Please try again.'); 88 + } finally { 89 + setIsSubmitting(false); 90 + setShowDeleteConfirm(false); 91 + } 92 + }; 93 + 94 + const handleBackdropClick = (e: React.MouseEvent) => { 95 + if (e.target === e.currentTarget && !isSubmitting) { 96 + onClose(); 97 + } 98 + }; 99 + 100 + return ( 101 + <div className={styles.modalBackdrop} onClick={handleBackdropClick}> 102 + <div className={styles.modalContent}> 103 + <div className={styles.modalHeader}> 104 + <h2>Edit Your Flush</h2> 105 + <button 106 + className={styles.closeButton} 107 + onClick={onClose} 108 + disabled={isSubmitting} 109 + aria-label="Close" 110 + > 111 + 112 + </button> 113 + </div> 114 + 115 + {error && ( 116 + <div className={styles.error}> 117 + {error} 118 + </div> 119 + )} 120 + 121 + <div className={styles.formGroup}> 122 + <label htmlFor="flush-text">Status Text</label> 123 + <input 124 + id="flush-text" 125 + type="text" 126 + value={text} 127 + onChange={(e) => setText(e.target.value)} 128 + placeholder="is flushing" 129 + maxLength={59} 130 + disabled={isSubmitting} 131 + className={styles.textInput} 132 + /> 133 + <div className={styles.charCount}> 134 + {text.length}/59 135 + </div> 136 + </div> 137 + 138 + <div className={styles.formGroup}> 139 + <label>Select Emoji</label> 140 + <div className={styles.emojiGrid}> 141 + {APPROVED_EMOJIS.map((emoji) => ( 142 + <button 143 + key={emoji} 144 + type="button" 145 + onClick={() => setSelectedEmoji(emoji)} 146 + className={`${styles.emojiButton} ${selectedEmoji === emoji ? styles.selected : ''}`} 147 + disabled={isSubmitting} 148 + > 149 + {emoji} 150 + </button> 151 + ))} 152 + </div> 153 + </div> 154 + 155 + <div className={styles.modalActions}> 156 + {!showDeleteConfirm ? ( 157 + <> 158 + <button 159 + onClick={() => setShowDeleteConfirm(true)} 160 + disabled={isSubmitting} 161 + className={styles.deleteButton} 162 + > 163 + Delete Flush 164 + </button> 165 + <div className={styles.rightActions}> 166 + <button 167 + onClick={onClose} 168 + disabled={isSubmitting} 169 + className={styles.cancelButton} 170 + > 171 + Cancel 172 + </button> 173 + <button 174 + onClick={handleSave} 175 + disabled={isSubmitting} 176 + className={styles.saveButton} 177 + > 178 + {isSubmitting ? 'Saving...' : 'Save Changes'} 179 + </button> 180 + </div> 181 + </> 182 + ) : ( 183 + <div className={styles.deleteConfirmation}> 184 + <p>Are you sure you want to delete this flush? This cannot be undone.</p> 185 + <div className={styles.confirmButtons}> 186 + <button 187 + onClick={() => setShowDeleteConfirm(false)} 188 + disabled={isSubmitting} 189 + className={styles.cancelButton} 190 + > 191 + Cancel 192 + </button> 193 + <button 194 + onClick={handleDelete} 195 + disabled={isSubmitting} 196 + className={styles.confirmDeleteButton} 197 + > 198 + {isSubmitting ? 'Deleting...' : 'Yes, Delete'} 199 + </button> 200 + </div> 201 + </div> 202 + )} 203 + </div> 204 + </div> 205 + </div> 206 + ); 207 + } 208 +
+92
src/lib/api-client.ts
··· 70 70 console.error('Failed to create post:', error); 71 71 throw error; 72 72 } 73 + } 74 + 75 + // Delete a flush record 76 + export async function deleteFlushRecord(session: OAuthSession, recordUri: string) { 77 + if (typeof window === 'undefined') { 78 + throw new Error('API client can only be used on the client side'); 79 + } 80 + 81 + try { 82 + console.log('Deleting flush record:', recordUri); 83 + 84 + // Create an Agent instance using the OAuth session 85 + const agent = new Agent(session); 86 + 87 + // Parse the AT URI to extract repo, collection, and rkey 88 + // Format: at://did:plc:xxx/collection.name/rkey 89 + const uriParts = recordUri.replace('at://', '').split('/'); 90 + if (uriParts.length !== 3) { 91 + throw new Error('Invalid record URI format'); 92 + } 93 + 94 + const [repo, collection, rkey] = uriParts; 95 + 96 + console.log('Deleting record:', { repo, collection, rkey }); 97 + 98 + // Delete the record 99 + const result = await agent.api.com.atproto.repo.deleteRecord({ 100 + repo, 101 + collection, 102 + rkey 103 + }); 104 + 105 + console.log('Record deleted successfully'); 106 + return result; 107 + } catch (error) { 108 + console.error('Failed to delete record:', error); 109 + throw error; 110 + } 111 + } 112 + 113 + // Update a flush record using putRecord 114 + export async function updateFlushRecord( 115 + session: OAuthSession, 116 + recordUri: string, 117 + text: string, 118 + emoji: string, 119 + originalCreatedAt?: string 120 + ) { 121 + if (typeof window === 'undefined') { 122 + throw new Error('API client can only be used on the client side'); 123 + } 124 + 125 + try { 126 + console.log('Updating flush record:', recordUri); 127 + 128 + // Create an Agent instance using the OAuth session 129 + const agent = new Agent(session); 130 + 131 + // Parse the AT URI 132 + const uriParts = recordUri.replace('at://', '').split('/'); 133 + if (uriParts.length !== 3) { 134 + throw new Error('Invalid record URI format'); 135 + } 136 + 137 + const [repo, collection, rkey] = uriParts; 138 + 139 + console.log('Updating record:', { repo, collection, rkey }); 140 + 141 + // Create the updated record 142 + const updatedRecord = { 143 + $type: 'im.flushing.right.now', 144 + text, 145 + emoji, 146 + createdAt: originalCreatedAt || new Date().toISOString(), 147 + }; 148 + 149 + console.log('Updated record data:', updatedRecord); 150 + 151 + // Update the record using putRecord 152 + const result = await agent.api.com.atproto.repo.putRecord({ 153 + repo, 154 + collection, 155 + rkey, 156 + record: updatedRecord 157 + }); 158 + 159 + console.log('Record updated successfully'); 160 + return result; 161 + } catch (error) { 162 + console.error('Failed to update record:', error); 163 + throw error; 164 + } 73 165 }