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