···
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
+
interface ClassResult {
+
section: string | null;
@customElement("class-registration-modal")
export class ClassRegistrationModal extends LitElement {
@property({ type: Boolean }) open = false;
+
@state() searchQuery = "";
+
@state() results: ClassResult[] = [];
+
@state() isSearching = false;
+
@state() isJoining = false;
+
@state() hasSearched = false;
static override styles = css`
···
border: 2px solid var(--secondary);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
···
background: var(--secondary);
+
.search-section > label {
+
.search-input-wrapper {
···
···
border-color: var(--primary);
+
padding: 0.75rem 1.5rem;
+
background: var(--primary);
+
border: 2px solid var(--primary);
+
.search-btn:hover:not(:disabled) {
+
background: var(--gunmetal);
+
border-color: var(--gunmetal);
···
+
background: var(--background);
+
border: 2px solid var(--secondary);
+
.class-card:hover:not(:disabled) {
+
border-color: var(--accent);
+
transform: translateX(4px);
+
justify-content: space-between;
+
align-items: flex-start;
+
text-transform: uppercase;
+
color: var(--paynes-gray);
background: var(--primary);
+
border: 2px solid var(--primary);
+
.join-btn:hover:not(:disabled) {
background: var(--gunmetal);
border-color: var(--gunmetal);
+
color: var(--paynes-gray);
+
color: var(--paynes-gray);
+
this.hasSearched = false;
this.dispatchEvent(new CustomEvent("close"));
private handleInput(e: Event) {
+
this.searchQuery = (e.target as HTMLInputElement).value;
+
private async handleSearch(e: Event) {
+
if (!this.searchQuery.trim()) return;
+
this.isSearching = true;
+
this.hasSearched = true;
+
const response = await fetch(
+
`/api/classes/search?q=${encodeURIComponent(this.searchQuery.trim())}`,
+
throw new Error("Search failed");
+
const data = await response.json();
+
this.results = data.classes || [];
+
this.error = "Failed to search classes. Please try again.";
+
this.isSearching = false;
+
private async handleJoin(classId: string) {
const response = await fetch("/api/classes/join", {
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ class_id: classId }),
···
this.error = "Failed to join class. Please try again.";
+
this.isJoining = false;
···
<div class="modal-overlay" @click=${this.handleClose}>
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
<div class="modal-header">
+
<h2 class="modal-title">Find a Class</h2>
<button class="close-btn" @click=${this.handleClose} type="button">×</button>
+
<div class="search-section">
+
<label for="search">Course Code</label>
+
<form class="search-form" @submit=${this.handleSearch}>
+
<div class="search-input-wrapper">
+
placeholder="CS 101, MATH 220, etc."
+
.value=${this.searchQuery}
+
@input=${this.handleInput}
+
?disabled=${this.isSearching}
+
?disabled=${this.isSearching || !this.searchQuery.trim()}
+
${this.isSearching ? "Searching..." : "Search"}
+
<div class="helper-text">
+
Search by course code to find available classes
+
${this.error ? html`<div class="error-message">${this.error}</div>` : ""}
+
<div class="results-section">
+
? html`<div class="loading">Searching...</div>`
+
: this.results.length === 0
+
<div class="empty-state">
+
No classes found matching "${this.searchQuery}"
+
<div class="results-grid">
+
@click=${() => this.handleJoin(cls.id)}
+
?disabled=${this.isJoining}
+
<div class="class-header">
+
<div class="class-info">
+
<div class="course-code">${cls.course_code}</div>
+
<div class="class-name">${cls.name}</div>
+
<div class="class-meta">
+
<span>👤 ${cls.professor}</span>
+
${cls.section ? html`<span>📍 Section ${cls.section}</span>` : ""}
+
<span>📅 ${cls.semester} ${cls.year}</span>
+
?disabled=${this.isJoining}
+
@click=${(e: Event) => {
+
this.handleJoin(cls.id);
+
${this.isJoining ? "Joining..." : "Join"}