馃 distributed transcription service thistle.dunkirk.sh
1import { css, html, LitElement } from "lit"; 2import { customElement, state } from "lit/decorators.js"; 3import "./class-registration-modal"; 4 5interface Class { 6 id: string; 7 course_code: string; 8 name: string; 9 professor: string; 10 semester: string; 11 year: number; 12 archived: boolean; 13} 14 15interface ClassesGrouped { 16 [semesterYear: string]: Class[]; 17} 18 19@customElement("classes-overview") 20export class ClassesOverview extends LitElement { 21 @state() classes: ClassesGrouped = {}; 22 @state() isLoading = true; 23 @state() error: string | null = null; 24 @state() showRegistrationModal = false; 25 26 static override styles = css` 27 :host { 28 display: block; 29 } 30 31 h1 { 32 color: var(--text); 33 margin-bottom: 2rem; 34 } 35 36 .semester-section { 37 margin-bottom: 3rem; 38 } 39 40 .semester-title { 41 font-size: 1.5rem; 42 font-weight: 600; 43 color: var(--primary); 44 margin-bottom: 1.5rem; 45 padding-bottom: 0.5rem; 46 border-bottom: 2px solid var(--secondary); 47 } 48 49 .classes-grid { 50 display: grid; 51 grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); 52 gap: 1.5rem; 53 } 54 55 .class-card { 56 background: var(--background); 57 border: 1px solid var(--secondary); 58 border-radius: 8px; 59 padding: 1.5rem; 60 cursor: pointer; 61 transition: all 0.2s; 62 text-decoration: none; 63 color: var(--text); 64 display: block; 65 position: relative; 66 } 67 68 .class-card:hover { 69 border-color: var(--accent); 70 transform: translateY(-2px); 71 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 72 } 73 74 .class-card.archived { 75 opacity: 0.6; 76 border-style: dashed; 77 } 78 79 .course-code { 80 font-size: 0.875rem; 81 font-weight: 600; 82 color: var(--accent); 83 text-transform: uppercase; 84 margin-bottom: 0.5rem; 85 } 86 87 .class-name { 88 font-size: 1.125rem; 89 font-weight: 600; 90 margin-bottom: 0.5rem; 91 color: var(--text); 92 } 93 94 .professor { 95 font-size: 0.875rem; 96 color: var(--paynes-gray); 97 margin-bottom: 0.25rem; 98 } 99 100 .archived-badge { 101 position: absolute; 102 top: 0.75rem; 103 right: 0.75rem; 104 background: var(--paynes-gray); 105 color: var(--white); 106 padding: 0.25rem 0.5rem; 107 border-radius: 4px; 108 font-size: 0.75rem; 109 font-weight: 600; 110 text-transform: uppercase; 111 } 112 113 .register-card { 114 background: color-mix(in srgb, var(--accent) 10%, transparent); 115 border: 2px dashed var(--accent); 116 border-radius: 8px; 117 padding: 1.5rem; 118 cursor: pointer; 119 transition: all 0.2s; 120 display: flex; 121 flex-direction: column; 122 align-items: center; 123 justify-content: center; 124 color: var(--accent); 125 } 126 127 .register-card:hover { 128 background: color-mix(in srgb, var(--accent) 20%, transparent); 129 transform: translateY(-2px); 130 } 131 132 .register-icon { 133 font-size: 3rem; 134 margin-bottom: 0.5rem; 135 } 136 137 .register-text { 138 font-weight: 600; 139 font-size: 1rem; 140 } 141 142 .empty-state { 143 text-align: center; 144 padding: 4rem 2rem; 145 color: var(--paynes-gray); 146 } 147 148 .empty-state h2 { 149 color: var(--text); 150 margin-bottom: 1rem; 151 } 152 153 .loading { 154 text-align: center; 155 padding: 4rem 2rem; 156 color: var(--paynes-gray); 157 } 158 159 .error { 160 background: color-mix(in srgb, red 10%, transparent); 161 border: 1px solid red; 162 color: red; 163 padding: 1rem; 164 border-radius: 4px; 165 margin-bottom: 2rem; 166 } 167 `; 168 169 override async connectedCallback() { 170 super.connectedCallback(); 171 await this.loadClasses(); 172 window.addEventListener("auth-changed", this.handleAuthChange); 173 } 174 175 override disconnectedCallback() { 176 super.disconnectedCallback(); 177 window.removeEventListener("auth-changed", this.handleAuthChange); 178 } 179 180 private handleAuthChange = async () => { 181 await this.loadClasses(); 182 }; 183 184 private async loadClasses() { 185 this.isLoading = true; 186 this.error = null; 187 188 try { 189 const response = await fetch("/api/classes"); 190 if (!response.ok) { 191 if (response.status === 401) { 192 this.classes = {}; 193 return; 194 } 195 throw new Error("Failed to load classes"); 196 } 197 198 const data = await response.json(); 199 this.classes = data.classes || {}; 200 } catch (error) { 201 console.error("Failed to load classes:", error); 202 this.error = "Failed to load classes. Please try again."; 203 } finally { 204 this.isLoading = false; 205 } 206 } 207 208 private handleRegisterClick() { 209 this.showRegistrationModal = true; 210 } 211 212 private handleModalClose() { 213 this.showRegistrationModal = false; 214 } 215 216 private async handleClassJoined() { 217 await this.loadClasses(); 218 } 219 220 override render() { 221 if (this.isLoading) { 222 return html`<div class="loading">Loading classes...</div>`; 223 } 224 225 if (this.error) { 226 return html` 227 <div class="error">${this.error}</div> 228 <button @click=${this.loadClasses}>Retry</button> 229 `; 230 } 231 232 const semesterKeys = Object.keys(this.classes); 233 const hasClasses = semesterKeys.length > 0; 234 235 return html` 236 <h1>Your Classes</h1> 237 238 ${ 239 hasClasses 240 ? html` 241 ${semesterKeys.map( 242 (semesterYear) => html` 243 <div class="semester-section"> 244 <h2 class="semester-title">${semesterYear}</h2> 245 <div class="classes-grid"> 246 ${this.classes[semesterYear]?.map( 247 (cls) => html` 248 <a class="class-card ${cls.archived ? "archived" : ""}" href="/classes/${cls.id}"> 249 ${cls.archived ? html`<div class="archived-badge">Archived</div>` : ""} 250 <div class="course-code">${cls.course_code}</div> 251 <div class="class-name">${cls.name}</div> 252 <div class="professor">${cls.professor}</div> 253 </a> 254 `, 255 )} 256 257 ${ 258 semesterKeys.indexOf(semesterYear) === 0 259 ? html` 260 <div class="register-card" @click=${this.handleRegisterClick}> 261 <div class="register-icon">+</div> 262 <div class="register-text">Register for Class</div> 263 </div> 264 ` 265 : "" 266 } 267 </div> 268 </div> 269 `, 270 )} 271 ` 272 : html` 273 <div class="empty-state"> 274 <h2>No classes yet</h2> 275 <p>You haven't been enrolled in any classes.</p> 276 </div> 277 <div class="classes-grid"> 278 <div class="register-card" @click=${this.handleRegisterClick}> 279 <div class="register-icon">+</div> 280 <div class="register-text">Register for Class</div> 281 </div> 282 </div> 283 ` 284 } 285 286 <class-registration-modal 287 ?open=${this.showRegistrationModal} 288 @close=${this.handleModalClose} 289 @class-joined=${this.handleClassJoined} 290 ></class-registration-modal> 291 `; 292 } 293}