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