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