···
1
+
import { css, html, LitElement } from "lit";
2
+
import { customElement, property, state } from "lit/decorators.js";
4
+
interface MeetingTime {
9
+
@customElement("upload-recording-modal")
10
+
export class UploadRecordingModal extends LitElement {
11
+
@property({ type: Boolean }) open = false;
12
+
@property({ type: String }) classId = "";
13
+
@property({ type: Array }) meetingTimes: MeetingTime[] = [];
15
+
@state() private selectedFile: File | null = null;
16
+
@state() private selectedMeetingTimeId: string | null = null;
17
+
@state() private uploading = false;
18
+
@state() private error: string | null = null;
20
+
static override styles = css`
35
+
background: rgba(0, 0, 0, 0.5);
37
+
align-items: center;
38
+
justify-content: center;
43
+
background: var(--background);
50
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
55
+
justify-content: space-between;
56
+
align-items: center;
57
+
margin-bottom: 1.5rem;
70
+
color: var(--paynes-gray);
76
+
align-items: center;
77
+
justify-content: center;
79
+
transition: background 0.2s;
82
+
.close-button:hover {
83
+
background: var(--secondary);
87
+
margin-bottom: 1.5rem;
94
+
margin-bottom: 0.5rem;
95
+
font-size: 0.875rem;
98
+
.file-input-wrapper {
100
+
border: 2px dashed var(--secondary);
101
+
border-radius: 8px;
103
+
text-align: center;
105
+
transition: all 0.2s;
108
+
.file-input-wrapper:hover {
109
+
border-color: var(--accent);
110
+
background: color-mix(in srgb, var(--accent) 5%, transparent);
113
+
.file-input-wrapper.has-file {
114
+
border-color: var(--accent);
115
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
118
+
input[type="file"] {
119
+
position: absolute;
128
+
.file-input-label {
129
+
color: var(--paynes-gray);
130
+
font-size: 0.875rem;
133
+
.file-input-label strong {
134
+
color: var(--accent);
138
+
margin-top: 0.5rem;
139
+
color: var(--text);
146
+
border: 1px solid var(--secondary);
147
+
border-radius: 4px;
148
+
font-size: 0.875rem;
149
+
color: var(--text);
150
+
background: var(--background);
156
+
border-color: var(--primary);
160
+
font-size: 0.75rem;
161
+
color: var(--paynes-gray);
162
+
margin-top: 0.25rem;
166
+
background: color-mix(in srgb, red 10%, transparent);
167
+
border: 1px solid red;
170
+
border-radius: 4px;
171
+
margin-bottom: 1rem;
172
+
font-size: 0.875rem;
178
+
justify-content: flex-end;
183
+
padding: 0.75rem 1.5rem;
184
+
border-radius: 4px;
185
+
font-size: 0.875rem;
188
+
transition: opacity 0.2s;
192
+
button:hover:not(:disabled) {
198
+
cursor: not-allowed;
202
+
background: var(--secondary);
203
+
color: var(--text);
207
+
background: var(--accent);
208
+
color: var(--white);
213
+
align-items: center;
218
+
private handleFileSelect(e: Event) {
219
+
const input = e.target as HTMLInputElement;
220
+
if (input.files && input.files.length > 0) {
221
+
this.selectedFile = input.files[0] ?? null;
226
+
private handleMeetingTimeChange(e: Event) {
227
+
const select = e.target as HTMLSelectElement;
228
+
this.selectedMeetingTimeId = select.value || null;
231
+
private handleClose() {
232
+
if (this.uploading) return;
234
+
this.selectedFile = null;
235
+
this.selectedMeetingTimeId = null;
237
+
this.dispatchEvent(new CustomEvent("close"));
240
+
private async handleUpload() {
241
+
if (!this.selectedFile) {
242
+
this.error = "Please select a file to upload";
246
+
if (!this.selectedMeetingTimeId) {
247
+
this.error = "Please select a meeting time";
251
+
this.uploading = true;
255
+
const formData = new FormData();
256
+
formData.append("audio", this.selectedFile);
257
+
formData.append("class_id", this.classId);
258
+
formData.append("meeting_time_id", this.selectedMeetingTimeId);
260
+
const response = await fetch("/api/transcriptions", {
265
+
if (!response.ok) {
266
+
const data = await response.json();
267
+
throw new Error(data.error || "Upload failed");
270
+
// Success - close modal and notify parent
271
+
this.dispatchEvent(new CustomEvent("upload-success"));
272
+
this.handleClose();
274
+
console.error("Upload failed:", error);
276
+
error instanceof Error ? error.message : "Upload failed. Please try again.";
278
+
this.uploading = false;
282
+
override render() {
283
+
if (!this.open) return null;
286
+
<div class="overlay" @click=${(e: Event) => e.target === e.currentTarget && this.handleClose()}>
287
+
<div class="modal">
288
+
<div class="modal-header">
289
+
<h2>Upload Recording</h2>
290
+
<button class="close-button" @click=${this.handleClose} ?disabled=${this.uploading}>
295
+
${this.error ? html`<div class="error">${this.error}</div>` : ""}
297
+
<form @submit=${(e: Event) => e.preventDefault()}>
298
+
<div class="form-group">
299
+
<label>Audio File</label>
300
+
<div class="file-input-wrapper ${this.selectedFile ? "has-file" : ""}">
303
+
accept="audio/*,video/mp4,.mp3,.wav,.m4a,.aac,.ogg,.webm,.flac"
304
+
@change=${this.handleFileSelect}
305
+
?disabled=${this.uploading}
307
+
<div class="file-input-label">
310
+
? html`<div class="selected-file">📎 ${this.selectedFile.name}</div>`
312
+
<div>📤 <strong>Choose a file</strong> or drag it here</div>
313
+
<div style="margin-top: 0.5rem; font-size: 0.75rem;">
314
+
Supported: MP3, WAV, M4A, AAC, OGG, WebM, FLAC, MP4
320
+
<div class="help-text">Maximum file size: 100MB</div>
323
+
<div class="form-group">
324
+
<label for="meeting-time">Meeting Time</label>
327
+
@change=${this.handleMeetingTimeChange}
328
+
?disabled=${this.uploading}
331
+
<option value="">Select a meeting time...</option>
332
+
${this.meetingTimes.map(
334
+
<option value=${meeting.id}>${meeting.label}</option>
338
+
<div class="help-text">
339
+
Select which meeting this recording is for
344
+
<div class="modal-footer">
345
+
<button class="btn-cancel" @click=${this.handleClose} ?disabled=${this.uploading}>
350
+
@click=${this.handleUpload}
351
+
?disabled=${this.uploading || !this.selectedFile || !this.selectedMeetingTimeId}
355
+
? html`<span class="uploading-text">Uploading...</span>`