馃 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}