🪻 distributed transcription service thistle.dunkirk.sh

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>

dunkirk.sh c9d3c316 de06cd2e

verified
Changed files
+135 -78
src
+135 -78
src/components/admin-pending-recordings.ts
···
margin-bottom: 1rem;
}
-
table {
-
width: 100%;
-
border-collapse: collapse;
background: var(--background);
border: 2px solid var(--secondary);
border-radius: 8px;
-
overflow: hidden;
}
-
thead {
-
background: var(--primary);
-
color: var(--white);
}
-
th {
-
padding: 1rem;
-
text-align: left;
-
font-weight: 600;
}
-
td {
-
padding: 1rem;
-
border-top: 1px solid var(--secondary);
color: var(--text);
}
-
tr:hover {
-
background: color-mix(in srgb, var(--primary) 5%, transparent);
}
.class-info {
···
color: var(--primary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
-
font-size: 0.75rem;
font-weight: 500;
}
···
}
.user-avatar {
-
width: 2rem;
-
height: 2rem;
border-radius: 50%;
}
···
font-size: 0.875rem;
}
.approve-btn {
background: var(--accent);
color: var(--white);
border: none;
-
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: opacity 0.2s;
}
.approve-btn:hover:not(:disabled) {
···
cursor: not-allowed;
}
-
.actions {
-
display: flex;
-
gap: 0.5rem;
-
}
-
.delete-btn {
background: transparent;
border: 2px solid #dc2626;
color: #dc2626;
-
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
···
}
return html`
-
<table>
-
<thead>
-
<tr>
-
<th>File Name</th>
-
<th>Class</th>
-
<th>Meeting Time</th>
-
<th>Uploaded By</th>
-
<th>Uploaded At</th>
-
<th>Actions</th>
-
</tr>
-
</thead>
-
<tbody>
-
${this.recordings.map(
-
(recording) => html`
-
<tr>
-
<td>${recording.original_filename}</td>
-
<td>
-
<div class="class-info">
-
<span class="course-code">${recording.course_code}</span>
-
<span class="class-name">${recording.class_name}</span>
</div>
-
</td>
-
<td>
-
${
-
recording.meeting_label
-
? html`<span class="meeting-label">${recording.meeting_label}</span>`
-
: html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>`
-
}
-
</td>
-
<td>
-
<div class="user-info">
-
<img
-
src="https://hostedboringavatars.vercel.app/api/marble?size=32&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff"
-
alt="Avatar"
-
class="user-avatar"
-
/>
-
<span>${recording.user_name || recording.user_email}</span>
</div>
-
</td>
-
<td class="timestamp">${this.formatTimestamp(recording.created_at)}</td>
-
<td>
-
<div class="actions">
-
<button class="approve-btn" @click=${() => this.handleApprove(recording.id)}>
-
✓ Approve & Transcribe
-
</button>
-
<button class="delete-btn" @click=${() => this.handleDelete(recording.id)}>
-
Delete
-
</button>
</div>
-
</td>
-
</tr>
-
`,
-
)}
-
</tbody>
-
</table>
`;
}
}
···
margin-bottom: 1rem;
}
+
.recordings-grid {
+
display: grid;
+
gap: 1.5rem;
+
}
+
+
.recording-card {
background: var(--background);
border: 2px solid var(--secondary);
border-radius: 8px;
+
padding: 1.5rem;
+
transition: border-color 0.2s;
}
+
.recording-card:hover {
+
border-color: var(--primary);
+
}
+
+
.card-header {
+
display: flex;
+
justify-content: space-between;
+
align-items: flex-start;
+
margin-bottom: 1rem;
}
+
.file-info {
+
flex: 1;
}
+
.filename {
+
font-size: 1.125rem;
+
font-weight: 600;
color: var(--text);
+
margin-bottom: 0.5rem;
}
+
.meta-row {
+
display: flex;
+
gap: 2rem;
+
flex-wrap: wrap;
+
margin-bottom: 1rem;
+
}
+
+
.meta-item {
+
display: flex;
+
flex-direction: column;
+
gap: 0.25rem;
+
}
+
+
.meta-label {
+
font-size: 0.75rem;
+
font-weight: 600;
+
text-transform: uppercase;
+
color: var(--paynes-gray);
+
letter-spacing: 0.05em;
+
}
+
+
.meta-value {
+
font-size: 0.875rem;
+
color: var(--text);
}
.class-info {
···
color: var(--primary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
+
font-size: 0.875rem;
font-weight: 500;
}
···
}
.user-avatar {
+
width: 1.5rem;
+
height: 1.5rem;
border-radius: 50%;
}
···
font-size: 0.875rem;
}
+
.audio-player {
+
margin: 1rem 0;
+
}
+
+
.audio-player audio {
+
width: 100%;
+
height: 2.5rem;
+
}
+
+
.actions {
+
display: flex;
+
gap: 0.75rem;
+
margin-top: 1rem;
+
}
+
.approve-btn {
background: var(--accent);
color: var(--white);
border: none;
+
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: opacity 0.2s;
+
flex: 1;
}
.approve-btn:hover:not(:disabled) {
···
cursor: not-allowed;
}
.delete-btn {
background: transparent;
border: 2px solid #dc2626;
color: #dc2626;
+
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
···
}
return html`
+
<div class="recordings-grid">
+
${this.recordings.map(
+
(recording) => html`
+
<div class="recording-card">
+
<div class="card-header">
+
<div class="file-info">
+
<div class="filename">${recording.original_filename}</div>
+
</div>
+
</div>
+
+
<div class="meta-row">
+
<div class="meta-item">
+
<div class="meta-label">Class</div>
+
<div class="meta-value">
+
<div class="class-info">
+
<span class="course-code">${recording.course_code}</span>
+
<span class="class-name">${recording.class_name}</span>
+
</div>
</div>
+
</div>
+
+
<div class="meta-item">
+
<div class="meta-label">Meeting Time</div>
+
<div class="meta-value">
+
${
+
recording.meeting_label
+
? html`<span class="meeting-label">${recording.meeting_label}</span>`
+
: html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>`
+
}
</div>
+
</div>
+
+
<div class="meta-item">
+
<div class="meta-label">Uploaded By</div>
+
<div class="meta-value">
+
<div class="user-info">
+
<img
+
src="https://hostedboringavatars.vercel.app/api/marble?size=24&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff"
+
alt="Avatar"
+
class="user-avatar"
+
/>
+
<span>${recording.user_name || recording.user_email}</span>
+
</div>
+
</div>
+
</div>
+
+
<div class="meta-item">
+
<div class="meta-label">Uploaded At</div>
+
<div class="meta-value timestamp">
+
${this.formatTimestamp(recording.created_at)}
</div>
+
</div>
+
</div>
+
+
<div class="audio-player">
+
<audio controls preload="metadata" src="/api/transcriptions/${recording.id}/audio"></audio>
+
</div>
+
+
<div class="actions">
+
<button class="approve-btn" @click=${() => this.handleApprove(recording.id)}>
+
✓ Approve & Transcribe
+
</button>
+
<button class="delete-btn" @click=${() => this.handleDelete(recording.id)}>
+
Delete
+
</button>
+
</div>
+
</div>
+
`,
+
)}
+
</div>
`;
}
}