馃 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 .empty-state button { 154 margin-top: 2rem; 155 padding: 0.75rem 2rem; 156 background: var(--accent); 157 color: var(--white); 158 border: none; 159 border-radius: 8px; 160 font-size: 1rem; 161 font-weight: 600; 162 cursor: pointer; 163 transition: all 0.2s; 164 } 165 166 .empty-state button:hover { 167 background: color-mix(in srgb, var(--accent) 90%, black); 168 transform: translateY(-2px); 169 } 170 171 .loading { 172 text-align: center; 173 padding: 4rem 2rem; 174 color: var(--paynes-gray); 175 } 176 177 .error { 178 background: color-mix(in srgb, red 10%, transparent); 179 border: 1px solid red; 180 color: red; 181 padding: 1rem; 182 border-radius: 4px; 183 margin-bottom: 2rem; 184 } 185 `; 186 187 override async connectedCallback() { 188 super.connectedCallback(); 189 await this.loadClasses(); 190 window.addEventListener("auth-changed", this.handleAuthChange); 191 } 192 193 override disconnectedCallback() { 194 super.disconnectedCallback(); 195 window.removeEventListener("auth-changed", this.handleAuthChange); 196 } 197 198 private handleAuthChange = async () => { 199 await this.loadClasses(); 200 }; 201 202 private async loadClasses() { 203 this.isLoading = true; 204 this.error = null; 205 206 try { 207 const response = await fetch("/api/classes"); 208 if (!response.ok) { 209 if (response.status === 401) { 210 this.classes = {}; 211 return; 212 } 213 throw new Error("Failed to load classes"); 214 } 215 216 const data = await response.json(); 217 this.classes = data.classes || {}; 218 } catch (error) { 219 console.error("Failed to load classes:", error); 220 this.error = "Failed to load classes. Please try again."; 221 } finally { 222 this.isLoading = false; 223 } 224 } 225 226 private handleRegisterClick() { 227 this.showRegistrationModal = true; 228 } 229 230 private handleModalClose() { 231 this.showRegistrationModal = false; 232 } 233 234 private async handleClassJoined() { 235 await this.loadClasses(); 236 } 237 238 override render() { 239 if (this.isLoading) { 240 return html`<div class="loading">Loading classes...</div>`; 241 } 242 243 if (this.error) { 244 return html` 245 <div class="error">${this.error}</div> 246 <button @click=${this.loadClasses}>Retry</button> 247 `; 248 } 249 250 const semesterKeys = Object.keys(this.classes); 251 const hasClasses = semesterKeys.length > 0; 252 253 return html` 254 <h1>Your Classes</h1> 255 256 ${ 257 hasClasses 258 ? html` 259 ${semesterKeys.map( 260 (semesterYear) => html` 261 <div class="semester-section"> 262 <h2 class="semester-title">${semesterYear}</h2> 263 <div class="classes-grid"> 264 ${this.classes[semesterYear]?.map( 265 (cls) => html` 266 <a class="class-card ${cls.archived ? "archived" : ""}" href="/classes/${cls.id}"> 267 ${cls.archived ? html`<div class="archived-badge">Archived</div>` : ""} 268 <div class="course-code">${cls.course_code}</div> 269 <div class="class-name">${cls.name}</div> 270 <div class="professor">${cls.professor}</div> 271 </a> 272 `, 273 )} 274 275 ${ 276 semesterKeys.indexOf(semesterYear) === 0 277 ? html` 278 <div class="register-card" @click=${this.handleRegisterClick}> 279 <div class="register-icon">+</div> 280 <div class="register-text">Register for a Class</div> 281 </div> 282 ` 283 : "" 284 } 285 </div> 286 </div> 287 `, 288 )} 289 ` 290 : html` 291 <div class="empty-state"> 292 <h2>No classes yet</h2> 293 <p>You haven't been enrolled in any classes.</p> 294 <button @click=${this.handleRegisterClick}>Register for a Class</button> 295 </div> 296 ` 297 } 298 299 <class-registration-modal 300 ?open=${this.showRegistrationModal} 301 @close=${this.handleModalClose} 302 @class-joined=${this.handleClassJoined} 303 ></class-registration-modal> 304 `; 305 } 306}