···
+
import { css, html, LitElement } from "lit";
+
import { customElement, property, state } from "lit/decorators.js";
+
interface MeetingTime {
+
@customElement("upload-recording-modal")
+
export class UploadRecordingModal extends LitElement {
+
@property({ type: Boolean }) open = false;
+
@property({ type: String }) classId = "";
+
@property({ type: Array }) meetingTimes: MeetingTime[] = [];
+
@state() private selectedFile: File | null = null;
+
@state() private selectedMeetingTimeId: string | null = null;
+
@state() private uploading = false;
+
@state() private error: string | null = null;
+
static override styles = css`
+
background: rgba(0, 0, 0, 0.5);
+
justify-content: center;
+
background: var(--background);
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
+
justify-content: space-between;
+
color: var(--paynes-gray);
+
justify-content: center;
+
transition: background 0.2s;
+
background: var(--secondary);
+
border: 2px dashed var(--secondary);
+
.file-input-wrapper:hover {
+
border-color: var(--accent);
+
background: color-mix(in srgb, var(--accent) 5%, transparent);
+
.file-input-wrapper.has-file {
+
border-color: var(--accent);
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
+
color: var(--paynes-gray);
+
.file-input-label strong {
+
border: 1px solid var(--secondary);
+
background: var(--background);
+
border-color: var(--primary);
+
color: var(--paynes-gray);
+
background: color-mix(in srgb, red 10%, transparent);
+
justify-content: flex-end;
+
padding: 0.75rem 1.5rem;
+
transition: opacity 0.2s;
+
button:hover:not(:disabled) {
+
background: var(--secondary);
+
background: var(--accent);
+
private handleFileSelect(e: Event) {
+
const input = e.target as HTMLInputElement;
+
if (input.files && input.files.length > 0) {
+
this.selectedFile = input.files[0] ?? null;
+
private handleMeetingTimeChange(e: Event) {
+
const select = e.target as HTMLSelectElement;
+
this.selectedMeetingTimeId = select.value || null;
+
private handleClose() {
+
if (this.uploading) return;
+
this.selectedFile = null;
+
this.selectedMeetingTimeId = null;
+
this.dispatchEvent(new CustomEvent("close"));
+
private async handleUpload() {
+
if (!this.selectedFile) {
+
this.error = "Please select a file to upload";
+
if (!this.selectedMeetingTimeId) {
+
this.error = "Please select a meeting time";
+
const formData = new FormData();
+
formData.append("audio", this.selectedFile);
+
formData.append("class_id", this.classId);
+
formData.append("meeting_time_id", this.selectedMeetingTimeId);
+
const response = await fetch("/api/transcriptions", {
+
const data = await response.json();
+
throw new Error(data.error || "Upload failed");
+
// Success - close modal and notify parent
+
this.dispatchEvent(new CustomEvent("upload-success"));
+
console.error("Upload failed:", error);
+
error instanceof Error ? error.message : "Upload failed. Please try again.";
+
this.uploading = false;
+
if (!this.open) return null;
+
<div class="overlay" @click=${(e: Event) => e.target === e.currentTarget && this.handleClose()}>
+
<div class="modal-header">
+
<h2>Upload Recording</h2>
+
<button class="close-button" @click=${this.handleClose} ?disabled=${this.uploading}>
+
${this.error ? html`<div class="error">${this.error}</div>` : ""}
+
<form @submit=${(e: Event) => e.preventDefault()}>
+
<div class="form-group">
+
<label>Audio File</label>
+
<div class="file-input-wrapper ${this.selectedFile ? "has-file" : ""}">
+
accept="audio/*,video/mp4,.mp3,.wav,.m4a,.aac,.ogg,.webm,.flac"
+
@change=${this.handleFileSelect}
+
?disabled=${this.uploading}
+
<div class="file-input-label">
+
? html`<div class="selected-file">📎 ${this.selectedFile.name}</div>`
+
<div>📤 <strong>Choose a file</strong> or drag it here</div>
+
<div style="margin-top: 0.5rem; font-size: 0.75rem;">
+
Supported: MP3, WAV, M4A, AAC, OGG, WebM, FLAC, MP4
+
<div class="help-text">Maximum file size: 100MB</div>
+
<div class="form-group">
+
<label for="meeting-time">Meeting Time</label>
+
@change=${this.handleMeetingTimeChange}
+
?disabled=${this.uploading}
+
<option value="">Select a meeting time...</option>
+
${this.meetingTimes.map(
+
<option value=${meeting.id}>${meeting.label}</option>
+
<div class="help-text">
+
Select which meeting this recording is for
+
<div class="modal-footer">
+
<button class="btn-cancel" @click=${this.handleClose} ?disabled=${this.uploading}>
+
@click=${this.handleUpload}
+
?disabled=${this.uploading || !this.selectedFile || !this.selectedMeetingTimeId}
+
? html`<span class="uploading-text">Uploading...</span>`