🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

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

refactor: redesign pending recordings as cards with audio players

- Replace table layout with card-based grid layout
- Add embedded audio player to each card for preview
- Organize metadata in labeled sections (Class, Meeting Time, Uploader, Upload Date)
- Improve visual hierarchy with larger filename and better spacing
- Make approve/delete buttons full-width for easier interaction
- Audio player uses /api/transcriptions/:id/audio endpoint

💘 Generated with Crush

Co-Authored-By: Crush <crush@charm.land>

+135 -78
+135 -78
src/components/admin-pending-recordings.ts
··· 43 43 margin-bottom: 1rem; 44 44 } 45 45 46 - table { 47 - width: 100%; 48 - border-collapse: collapse; 46 + .recordings-grid { 47 + display: grid; 48 + gap: 1.5rem; 49 + } 50 + 51 + .recording-card { 49 52 background: var(--background); 50 53 border: 2px solid var(--secondary); 51 54 border-radius: 8px; 52 - overflow: hidden; 55 + padding: 1.5rem; 56 + transition: border-color 0.2s; 53 57 } 54 58 55 - thead { 56 - background: var(--primary); 57 - color: var(--white); 59 + .recording-card:hover { 60 + border-color: var(--primary); 61 + } 62 + 63 + .card-header { 64 + display: flex; 65 + justify-content: space-between; 66 + align-items: flex-start; 67 + margin-bottom: 1rem; 58 68 } 59 69 60 - th { 61 - padding: 1rem; 62 - text-align: left; 63 - font-weight: 600; 70 + .file-info { 71 + flex: 1; 64 72 } 65 73 66 - td { 67 - padding: 1rem; 68 - border-top: 1px solid var(--secondary); 74 + .filename { 75 + font-size: 1.125rem; 76 + font-weight: 600; 69 77 color: var(--text); 78 + margin-bottom: 0.5rem; 70 79 } 71 80 72 - tr:hover { 73 - background: color-mix(in srgb, var(--primary) 5%, transparent); 81 + .meta-row { 82 + display: flex; 83 + gap: 2rem; 84 + flex-wrap: wrap; 85 + margin-bottom: 1rem; 86 + } 87 + 88 + .meta-item { 89 + display: flex; 90 + flex-direction: column; 91 + gap: 0.25rem; 92 + } 93 + 94 + .meta-label { 95 + font-size: 0.75rem; 96 + font-weight: 600; 97 + text-transform: uppercase; 98 + color: var(--paynes-gray); 99 + letter-spacing: 0.05em; 100 + } 101 + 102 + .meta-value { 103 + font-size: 0.875rem; 104 + color: var(--text); 74 105 } 75 106 76 107 .class-info { ··· 96 127 color: var(--primary); 97 128 padding: 0.25rem 0.5rem; 98 129 border-radius: 4px; 99 - font-size: 0.75rem; 130 + font-size: 0.875rem; 100 131 font-weight: 500; 101 132 } 102 133 ··· 107 138 } 108 139 109 140 .user-avatar { 110 - width: 2rem; 111 - height: 2rem; 141 + width: 1.5rem; 142 + height: 1.5rem; 112 143 border-radius: 50%; 113 144 } 114 145 ··· 117 148 font-size: 0.875rem; 118 149 } 119 150 151 + .audio-player { 152 + margin: 1rem 0; 153 + } 154 + 155 + .audio-player audio { 156 + width: 100%; 157 + height: 2.5rem; 158 + } 159 + 160 + .actions { 161 + display: flex; 162 + gap: 0.75rem; 163 + margin-top: 1rem; 164 + } 165 + 120 166 .approve-btn { 121 167 background: var(--accent); 122 168 color: var(--white); 123 169 border: none; 124 - padding: 0.5rem 1rem; 170 + padding: 0.75rem 1.5rem; 125 171 border-radius: 4px; 126 172 cursor: pointer; 127 173 font-size: 0.875rem; 128 174 font-weight: 600; 129 175 transition: opacity 0.2s; 176 + flex: 1; 130 177 } 131 178 132 179 .approve-btn:hover:not(:disabled) { ··· 138 185 cursor: not-allowed; 139 186 } 140 187 141 - .actions { 142 - display: flex; 143 - gap: 0.5rem; 144 - } 145 - 146 188 .delete-btn { 147 189 background: transparent; 148 190 border: 2px solid #dc2626; 149 191 color: #dc2626; 150 - padding: 0.5rem 1rem; 192 + padding: 0.75rem 1.5rem; 151 193 border-radius: 4px; 152 194 cursor: pointer; 153 195 font-size: 0.875rem; ··· 322 364 } 323 365 324 366 return html` 325 - <table> 326 - <thead> 327 - <tr> 328 - <th>File Name</th> 329 - <th>Class</th> 330 - <th>Meeting Time</th> 331 - <th>Uploaded By</th> 332 - <th>Uploaded At</th> 333 - <th>Actions</th> 334 - </tr> 335 - </thead> 336 - <tbody> 337 - ${this.recordings.map( 338 - (recording) => html` 339 - <tr> 340 - <td>${recording.original_filename}</td> 341 - <td> 342 - <div class="class-info"> 343 - <span class="course-code">${recording.course_code}</span> 344 - <span class="class-name">${recording.class_name}</span> 367 + <div class="recordings-grid"> 368 + ${this.recordings.map( 369 + (recording) => html` 370 + <div class="recording-card"> 371 + <div class="card-header"> 372 + <div class="file-info"> 373 + <div class="filename">${recording.original_filename}</div> 374 + </div> 375 + </div> 376 + 377 + <div class="meta-row"> 378 + <div class="meta-item"> 379 + <div class="meta-label">Class</div> 380 + <div class="meta-value"> 381 + <div class="class-info"> 382 + <span class="course-code">${recording.course_code}</span> 383 + <span class="class-name">${recording.class_name}</span> 384 + </div> 345 385 </div> 346 - </td> 347 - <td> 348 - ${ 349 - recording.meeting_label 350 - ? html`<span class="meeting-label">${recording.meeting_label}</span>` 351 - : html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>` 352 - } 353 - </td> 354 - <td> 355 - <div class="user-info"> 356 - <img 357 - src="https://hostedboringavatars.vercel.app/api/marble?size=32&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff" 358 - alt="Avatar" 359 - class="user-avatar" 360 - /> 361 - <span>${recording.user_name || recording.user_email}</span> 386 + </div> 387 + 388 + <div class="meta-item"> 389 + <div class="meta-label">Meeting Time</div> 390 + <div class="meta-value"> 391 + ${ 392 + recording.meeting_label 393 + ? html`<span class="meeting-label">${recording.meeting_label}</span>` 394 + : html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>` 395 + } 362 396 </div> 363 - </td> 364 - <td class="timestamp">${this.formatTimestamp(recording.created_at)}</td> 365 - <td> 366 - <div class="actions"> 367 - <button class="approve-btn" @click=${() => this.handleApprove(recording.id)}> 368 - ✓ Approve & Transcribe 369 - </button> 370 - <button class="delete-btn" @click=${() => this.handleDelete(recording.id)}> 371 - Delete 372 - </button> 397 + </div> 398 + 399 + <div class="meta-item"> 400 + <div class="meta-label">Uploaded By</div> 401 + <div class="meta-value"> 402 + <div class="user-info"> 403 + <img 404 + src="https://hostedboringavatars.vercel.app/api/marble?size=24&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff" 405 + alt="Avatar" 406 + class="user-avatar" 407 + /> 408 + <span>${recording.user_name || recording.user_email}</span> 409 + </div> 410 + </div> 411 + </div> 412 + 413 + <div class="meta-item"> 414 + <div class="meta-label">Uploaded At</div> 415 + <div class="meta-value timestamp"> 416 + ${this.formatTimestamp(recording.created_at)} 373 417 </div> 374 - </td> 375 - </tr> 376 - `, 377 - )} 378 - </tbody> 379 - </table> 418 + </div> 419 + </div> 420 + 421 + <div class="audio-player"> 422 + <audio controls preload="metadata" src="/api/transcriptions/${recording.id}/audio"></audio> 423 + </div> 424 + 425 + <div class="actions"> 426 + <button class="approve-btn" @click=${() => this.handleApprove(recording.id)}> 427 + ✓ Approve & Transcribe 428 + </button> 429 + <button class="delete-btn" @click=${() => this.handleDelete(recording.id)}> 430 + Delete 431 + </button> 432 + </div> 433 + </div> 434 + `, 435 + )} 436 + </div> 380 437 `; 381 438 } 382 439 }