Luca448 commited on
Commit
dfa6a9f
·
1 Parent(s): 27b58fe

Savestate: Implement Calendar feature (apple style) with exams/homework tracking and Campus widget. (BUILD 1.6.0)

Browse files
index.html CHANGED
@@ -51,7 +51,7 @@
51
  </head>
52
  <body class="dark-theme in-chat-view">
53
 
54
- <div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.5.40</div>
55
 
56
  <!-- SVG Filters for Liquid Glass Refraction -->
57
  <svg width="0" height="0" style="position: absolute; pointer-events: none;">
 
51
  </head>
52
  <body class="dark-theme in-chat-view">
53
 
54
+ <div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.6.0</div>
55
 
56
  <!-- SVG Filters for Liquid Glass Refraction -->
57
  <svg width="0" height="0" style="position: absolute; pointer-events: none;">
src/app.js CHANGED
@@ -516,6 +516,17 @@ const initApp = async () => {
516
  handleImportError(e);
517
  }
518
  }
 
 
 
 
 
 
 
 
 
 
 
519
  else if (viewName === 'IServ') {
520
  inputCapsule.style.display = 'none';
521
  try {
 
516
  handleImportError(e);
517
  }
518
  }
519
+ else if (viewName === 'Kalender') {
520
+ inputCapsule.style.display = 'none';
521
+ try {
522
+ const { CalendarView } = await import('./views/calendar/calendar-view.js');
523
+ const calView = new CalendarView('router-view');
524
+ await calView.init();
525
+ activeView = calView;
526
+ } catch (e) {
527
+ handleImportError(e);
528
+ }
529
+ }
530
  else if (viewName === 'IServ') {
531
  inputCapsule.style.display = 'none';
532
  try {
src/db/database.js CHANGED
@@ -162,6 +162,17 @@ class Database {
162
  deleted_at TEXT
163
  );
164
 
 
 
 
 
 
 
 
 
 
 
 
165
  CREATE TABLE IF NOT EXISTS flashcard_decks (
166
  id TEXT PRIMARY KEY,
167
  title TEXT,
@@ -539,6 +550,30 @@ class Database {
539
  );
540
  }
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  }
543
 
544
  export const db = new Database();
 
162
  deleted_at TEXT
163
  );
164
 
165
+ CREATE TABLE IF NOT EXISTS calendar_events (
166
+ id TEXT PRIMARY KEY,
167
+ title TEXT,
168
+ type TEXT,
169
+ date TEXT,
170
+ time TEXT,
171
+ subject TEXT,
172
+ description TEXT,
173
+ created_at TEXT
174
+ );
175
+
176
  CREATE TABLE IF NOT EXISTS flashcard_decks (
177
  id TEXT PRIMARY KEY,
178
  title TEXT,
 
550
  );
551
  }
552
 
553
+ // --- Calendar ---
554
+ getCalendarEvents() {
555
+ return this.exec("SELECT * FROM calendar_events ORDER BY date ASC, time ASC");
556
+ }
557
+
558
+ getUpcomingEvents(limit = 4) {
559
+ const today = new Date().toISOString().split('T')[0];
560
+ return this.exec("SELECT * FROM calendar_events WHERE date >= ? ORDER BY date ASC, time ASC LIMIT ?", [today, limit]);
561
+ }
562
+
563
+ addCalendarEvent(title, type, date, time, subject, description) {
564
+ const id = 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
565
+ const now = new Date().toISOString();
566
+ this.exec(
567
+ "INSERT INTO calendar_events (id, title, type, date, time, subject, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
568
+ [id, title, type, date, time, subject, description, now]
569
+ );
570
+ return id;
571
+ }
572
+
573
+ deleteCalendarEvent(id) {
574
+ this.exec("DELETE FROM calendar_events WHERE id = ?", [id]);
575
+ }
576
+
577
  }
578
 
579
  export const db = new Database();
src/views/calendar/calendar-view.js ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { db } from '../../db/database.js';
2
+
3
+ export class CalendarView {
4
+ constructor(containerId) {
5
+ this.container = document.getElementById(containerId);
6
+ this.currentDate = new Date();
7
+ this.selectedDate = new Date(); // Default to today
8
+ this.events = [];
9
+ }
10
+
11
+ async init() {
12
+ this.loadEvents();
13
+ this.render();
14
+ }
15
+
16
+ loadEvents() {
17
+ this.events = db.getCalendarEvents();
18
+ }
19
+
20
+ getEventsForDate(dateString) {
21
+ return this.events.filter(e => e.date === dateString);
22
+ }
23
+
24
+ formatDate(date) {
25
+ // Returns YYYY-MM-DD
26
+ const y = date.getFullYear();
27
+ const m = String(date.getMonth() + 1).padStart(2, '0');
28
+ const d = String(date.getDate()).padStart(2, '0');
29
+ return `${y}-${m}-${d}`;
30
+ }
31
+
32
+ render() {
33
+ const monthNames = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
34
+ const monthName = monthNames[this.currentDate.getMonth()];
35
+ const year = this.currentDate.getFullYear();
36
+
37
+ this.container.innerHTML = `
38
+ <div class="calendar-container fade-in" style="padding-top: calc(env(safe-area-inset-top, 20px) + 20px); min-height: 100%; display: flex; flex-direction: column;">
39
+ <!-- Header -->
40
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 0 24px; margin-bottom: 24px;">
41
+ <div style="display: flex; align-items: center; gap: 12px;">
42
+ <button class="icon-btn" id="cal-back-btn" style="background: rgba(255,255,255,0.05);">
43
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
44
+ </button>
45
+ <h1 style="font-size: 28px; font-weight: 700; margin: 0;">${monthName} ${year}</h1>
46
+ </div>
47
+ <div style="display: flex; align-items: center; gap: 8px;">
48
+ <button id="cal-today-btn" style="background: rgba(255,59,48,0.15); color: #ff3b30; border: none; padding: 6px 12px; border-radius: 12px; font-weight: 600; font-size: 14px; cursor: pointer;">Heute</button>
49
+ <button id="cal-add-btn" class="icon-btn" style="background: #ff3b30; color: white;">
50
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
51
+ </button>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Month Navigation -->
56
+ <div style="display: flex; justify-content: space-between; padding: 0 24px; margin-bottom: 16px;">
57
+ <button id="cal-prev-month" class="icon-btn" style="background: transparent;"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg></button>
58
+ <button id="cal-next-month" class="icon-btn" style="background: transparent;"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg></button>
59
+ </div>
60
+
61
+ <!-- Calendar Grid -->
62
+ <div style="padding: 0 16px; flex-shrink: 0;">
63
+ <div style="background: rgba(30,30,35,0.6); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 24px; border: 1px solid rgba(255,255,255,0.05); padding: 16px;">
64
+ <div style="display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; color: rgba(255,255,255,0.4); font-size: 12px; font-weight: 600; margin-bottom: 12px;">
65
+ <div>Mo</div><div>Di</div><div>Mi</div><div>Do</div><div>Fr</div><div>Sa</div><div>So</div>
66
+ </div>
67
+ <div id="calendar-days" style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px;">
68
+ <!-- Days injected here -->
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Events List -->
74
+ <div style="flex-grow: 1; padding: 24px 16px; overflow-y: auto;">
75
+ <h2 id="selected-date-title" style="font-size: 18px; font-weight: 600; margin: 0 0 16px 0; color: rgba(255,255,255,0.8);"></h2>
76
+ <div id="events-list" style="display: flex; flex-direction: column; gap: 12px;">
77
+ <!-- Events injected here -->
78
+ </div>
79
+ </div>
80
+ </div>
81
+ `;
82
+
83
+ this.bindEvents();
84
+ this.renderCalendarGrid();
85
+ this.renderSelectedDayEvents();
86
+ }
87
+
88
+ bindEvents() {
89
+ document.getElementById('cal-back-btn').onclick = () => window.openView('Hub');
90
+ document.getElementById('cal-prev-month').onclick = () => {
91
+ this.currentDate.setMonth(this.currentDate.getMonth() - 1);
92
+ this.render();
93
+ };
94
+ document.getElementById('cal-next-month').onclick = () => {
95
+ this.currentDate.setMonth(this.currentDate.getMonth() + 1);
96
+ this.render();
97
+ };
98
+ document.getElementById('cal-today-btn').onclick = () => {
99
+ this.currentDate = new Date();
100
+ this.selectedDate = new Date();
101
+ this.render();
102
+ };
103
+ document.getElementById('cal-add-btn').onclick = () => {
104
+ this.showAddEventModal();
105
+ };
106
+ }
107
+
108
+ renderCalendarGrid() {
109
+ const daysContainer = document.getElementById('calendar-days');
110
+ daysContainer.innerHTML = '';
111
+
112
+ const year = this.currentDate.getFullYear();
113
+ const month = this.currentDate.getMonth();
114
+
115
+ const firstDay = new Date(year, month, 1);
116
+ const lastDay = new Date(year, month + 1, 0);
117
+
118
+ // Day of week for 1st of month (0 = Sunday, 1 = Monday)
119
+ let firstDayIndex = firstDay.getDay() - 1;
120
+ if (firstDayIndex === -1) firstDayIndex = 6; // Sunday is 6 in our Mo-So grid
121
+
122
+ const totalDays = lastDay.getDate();
123
+
124
+ const today = new Date();
125
+ const todayStr = this.formatDate(today);
126
+ const selectedStr = this.formatDate(this.selectedDate);
127
+
128
+ // Empty slots for days from previous month
129
+ for (let i = 0; i < firstDayIndex; i++) {
130
+ const emptyDiv = document.createElement('div');
131
+ daysContainer.appendChild(emptyDiv);
132
+ }
133
+
134
+ // Actual days
135
+ for (let i = 1; i <= totalDays; i++) {
136
+ const cellDate = new Date(year, month, i);
137
+ const cellDateStr = this.formatDate(cellDate);
138
+
139
+ const dayDiv = document.createElement('div');
140
+ dayDiv.className = 'cal-day-cell';
141
+
142
+ let isToday = cellDateStr === todayStr;
143
+ let isSelected = cellDateStr === selectedStr;
144
+
145
+ let styles = 'display: flex; flex-direction: column; align-items: center; justify-content: center; height: 44px; border-radius: 50%; font-size: 16px; cursor: pointer; position: relative; font-weight: 500; transition: all 0.2s;';
146
+
147
+ if (isSelected && !isToday) {
148
+ styles += ' background: rgba(255,255,255,0.15);';
149
+ }
150
+
151
+ const daySpan = document.createElement('span');
152
+ daySpan.innerText = i;
153
+
154
+ if (isToday) {
155
+ daySpan.style.cssText = 'background: #ff3b30; color: white; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%;';
156
+ }
157
+
158
+ dayDiv.style.cssText = styles;
159
+ dayDiv.appendChild(daySpan);
160
+
161
+ // Event dots
162
+ const dayEvents = this.getEventsForDate(cellDateStr);
163
+ if (dayEvents.length > 0) {
164
+ const dotsContainer = document.createElement('div');
165
+ dotsContainer.style.cssText = 'display: flex; gap: 3px; position: absolute; bottom: 4px;';
166
+
167
+ // Show max 3 dots
168
+ dayEvents.slice(0, 3).forEach(ev => {
169
+ const dot = document.createElement('div');
170
+ dot.style.cssText = 'width: 5px; height: 5px; border-radius: 50%;';
171
+ if (ev.type === 'exam') dot.style.background = '#ff3b30'; // Red
172
+ else if (ev.type === 'homework') dot.style.background = '#0a84ff'; // Blue
173
+ else dot.style.background = '#30d158'; // Green
174
+ dotsContainer.appendChild(dot);
175
+ });
176
+ dayDiv.appendChild(dotsContainer);
177
+ }
178
+
179
+ dayDiv.onclick = () => {
180
+ this.selectedDate = new Date(year, month, i);
181
+ this.renderCalendarGrid(); // Just re-render grid
182
+ this.renderSelectedDayEvents(); // And events list
183
+ };
184
+
185
+ daysContainer.appendChild(dayDiv);
186
+ }
187
+ }
188
+
189
+ renderSelectedDayEvents() {
190
+ const titleEl = document.getElementById('selected-date-title');
191
+ const listEl = document.getElementById('events-list');
192
+
193
+ const selectedStr = this.formatDate(this.selectedDate);
194
+ const dayEvents = this.getEventsForDate(selectedStr);
195
+
196
+ const options = { weekday: 'long', day: 'numeric', month: 'long' };
197
+ titleEl.innerText = this.selectedDate.toLocaleDateString('de-DE', options);
198
+
199
+ listEl.innerHTML = '';
200
+
201
+ if (dayEvents.length === 0) {
202
+ listEl.innerHTML = '<div style="color: rgba(255,255,255,0.4); font-size: 14px; text-align: center; padding: 20px;">Keine Termine an diesem Tag.</div>';
203
+ return;
204
+ }
205
+
206
+ dayEvents.forEach(ev => {
207
+ const item = document.createElement('div');
208
+ item.style.cssText = 'background: rgba(30,30,35,0.6); backdrop-filter: blur(20px); border-radius: 16px; padding: 16px; border: 1px solid rgba(255,255,255,0.05); position: relative; overflow: hidden;';
209
+
210
+ let color = '#30d158';
211
+ let typeLabel = 'Sonstiges';
212
+ if (ev.type === 'exam') { color = '#ff3b30'; typeLabel = 'Klausur'; }
213
+ if (ev.type === 'homework') { color = '#0a84ff'; typeLabel = 'Hausaufgabe'; }
214
+
215
+ item.innerHTML = `
216
+ <div style="position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: ${color};"></div>
217
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; padding-left: 8px;">
218
+ <div>
219
+ <div style="font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: ${color}; margin-bottom: 4px;">${typeLabel}</div>
220
+ <div style="font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 2px;">${ev.title.replace(/</g, '&lt;')}</div>
221
+ <div style="font-size: 13px; color: rgba(255,255,255,0.6);">${(ev.subject || 'Kein Fach').replace(/</g, '&lt;')} ${ev.time ? ' • ' + ev.time : ''}</div>
222
+ </div>
223
+ <button class="icon-btn delete-event-btn" data-id="${ev.id}" style="width: 28px; height: 28px; background: rgba(255,59,48,0.1); color: #ff3b30; flex-shrink: 0;">
224
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
225
+ </button>
226
+ </div>
227
+ ${ev.description ? `<div style="font-size: 13px; color: rgba(255,255,255,0.8); margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-left: 8px;">${ev.description.replace(/</g, '&lt;')}</div>` : ''}
228
+ `;
229
+
230
+ item.querySelector('.delete-event-btn').onclick = () => {
231
+ if(confirm('Termin wirklich löschen?')) {
232
+ db.deleteCalendarEvent(ev.id);
233
+ this.loadEvents();
234
+ this.renderCalendarGrid();
235
+ this.renderSelectedDayEvents();
236
+ }
237
+ };
238
+
239
+ listEl.appendChild(item);
240
+ });
241
+ }
242
+
243
+ showAddEventModal() {
244
+ const modal = document.createElement('div');
245
+ modal.className = 'modal-overlay fade-in';
246
+ modal.style.cssText = 'position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 9999; display: flex; align-items: flex-end; justify-content: center;';
247
+
248
+ modal.innerHTML = `
249
+ <div style="background: #1e1e23; width: 100%; max-width: 500px; border-radius: 24px 24px 0 0; padding: 24px; box-shadow: 0 -10px 40px rgba(0,0,0,0.3); animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);">
250
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
251
+ <h2 style="font-size: 20px; font-weight: 700; margin: 0;">Neuer Termin</h2>
252
+ <button id="cal-modal-close" class="icon-btn" style="background: rgba(255,255,255,0.05);"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
253
+ </div>
254
+
255
+ <div style="display: flex; flex-direction: column; gap: 16px;">
256
+ <div>
257
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">TITEL</label>
258
+ <input type="text" id="ev-title" placeholder="z.B. Mathe Klausur" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px;">
259
+ </div>
260
+
261
+ <div style="display: flex; gap: 12px;">
262
+ <div style="flex: 1;">
263
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">TYP</label>
264
+ <select id="ev-type" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px; appearance: none;">
265
+ <option value="exam">Klausur</option>
266
+ <option value="homework">Hausaufgabe</option>
267
+ <option value="general">Sonstiges</option>
268
+ </select>
269
+ </div>
270
+ <div style="flex: 1;">
271
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">FACH (OPTIONAL)</label>
272
+ <input type="text" id="ev-subject" placeholder="z.B. Mathe" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px;">
273
+ </div>
274
+ </div>
275
+
276
+ <div style="display: flex; gap: 12px;">
277
+ <div style="flex: 1;">
278
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">DATUM</label>
279
+ <input type="date" id="ev-date" value="${this.formatDate(this.selectedDate)}" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px; color-scheme: dark;">
280
+ </div>
281
+ <div style="flex: 1;">
282
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">ZEIT (OPTIONAL)</label>
283
+ <input type="time" id="ev-time" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px; color-scheme: dark;">
284
+ </div>
285
+ </div>
286
+
287
+ <div>
288
+ <label style="font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 6px; display: block;">NOTIZEN (OPTIONAL)</label>
289
+ <textarea id="ev-desc" rows="2" style="width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; color: white; outline: none; font-size: 15px; resize: none;"></textarea>
290
+ </div>
291
+
292
+ <button id="cal-modal-save" style="background: #0a84ff; color: white; border: none; padding: 16px; border-radius: 16px; font-weight: 600; font-size: 16px; cursor: pointer; margin-top: 8px;">Speichern</button>
293
+ </div>
294
+ </div>
295
+ `;
296
+
297
+ document.body.appendChild(modal);
298
+
299
+ const close = () => {
300
+ modal.classList.replace('fade-in', 'fade-out');
301
+ setTimeout(() => modal.remove(), 300);
302
+ };
303
+
304
+ modal.querySelector('#cal-modal-close').onclick = close;
305
+
306
+ modal.querySelector('#cal-modal-save').onclick = () => {
307
+ const title = document.getElementById('ev-title').value.trim();
308
+ if (!title) {
309
+ alert('Bitte einen Titel eingeben.');
310
+ return;
311
+ }
312
+
313
+ const type = document.getElementById('ev-type').value;
314
+ const subject = document.getElementById('ev-subject').value.trim();
315
+ const date = document.getElementById('ev-date').value;
316
+ const time = document.getElementById('ev-time').value;
317
+ const desc = document.getElementById('ev-desc').value.trim();
318
+
319
+ db.addCalendarEvent(title, type, date, time, subject, desc);
320
+
321
+ this.loadEvents();
322
+ this.selectedDate = new Date(date);
323
+ this.currentDate = new Date(date);
324
+ this.render(); // Full re-render to jump to that month
325
+ close();
326
+ };
327
+ }
328
+
329
+ destroy() {
330
+ this.container.innerHTML = '';
331
+ }
332
+ }
src/views/hub/hub-view.js CHANGED
@@ -1,3 +1,5 @@
 
 
1
  export class HubView {
2
  constructor(containerId) {
3
  this.container = document.getElementById(containerId);
@@ -8,8 +10,35 @@ export class HubView {
8
  }
9
 
10
  render() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  this.container.innerHTML = `
12
- <div class="hub-container fade-in" style="padding-top: calc(env(safe-area-inset-top, 20px) + 20px); min-height: 100%; padding-bottom: 32px;">
13
  <div style="padding: 0 24px; margin-bottom: 24px;">
14
  <h1 style="font-size: 32px; font-weight: 700; margin: 0; background: linear-gradient(to right, #fff, #a0a0a0); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Campus</h1>
15
  <p style="color: var(--text-secondary); margin-top: 4px; font-size: 15px;">Deine Schul-Tools auf einen Blick</p>
@@ -49,8 +78,34 @@ export class HubView {
49
  <p style="font-size: 12px; color: rgba(255,255,255,0.6); margin: 0;">Dokumente digitalisieren</p>
50
  </div>
51
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <!-- Optional: A horizontal card below the main grid -->
55
  <div style="padding: 0 16px;">
56
  <div class="bento-card card-insights" style="margin-top: 16px; flex-direction: row; align-items: center; justify-content: space-between; padding: 20px;">
@@ -68,7 +123,7 @@ export class HubView {
68
  .bento-grid {
69
  display: grid;
70
  grid-template-columns: 1.3fr 1fr;
71
- grid-template-rows: 150px 150px;
72
  gap: 16px;
73
  padding: 0 16px;
74
  }
@@ -99,9 +154,6 @@ export class HubView {
99
  margin-bottom: auto;
100
  z-index: 2;
101
  }
102
- .card-learn .bento-icon-bg {
103
- /* removed explicit sizes since it doesn't need a background anymore */
104
- }
105
  .bento-content {
106
  z-index: 2;
107
  position: relative;
@@ -121,6 +173,19 @@ export class HubView {
121
  grid-row: 2 / 3;
122
  background: linear-gradient(160deg, rgba(255, 107, 107, 0.15) 0%, rgba(30, 30, 35, 0.8) 100%);
123
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  .card-insights {
125
  background: linear-gradient(90deg, rgba(30, 30, 35, 0.8) 0%, rgba(193, 131, 240, 0.1) 100%);
126
  }
 
1
+ import { db } from '../../db/database.js';
2
+
3
  export class HubView {
4
  constructor(containerId) {
5
  this.container = document.getElementById(containerId);
 
10
  }
11
 
12
  render() {
13
+ const upcomingEvents = db.getUpcomingEvents(3);
14
+
15
+ let upcomingHtml = '';
16
+ if (upcomingEvents.length === 0) {
17
+ upcomingHtml = '<div style="color: rgba(255,255,255,0.4); font-size: 13px; text-align: center; padding: 10px;">Keine anstehenden Termine.</div>';
18
+ } else {
19
+ upcomingEvents.forEach(ev => {
20
+ let color = '#30d158';
21
+ if (ev.type === 'exam') color = '#ff3b30';
22
+ if (ev.type === 'homework') color = '#0a84ff';
23
+
24
+ const evDate = new Date(ev.date);
25
+ const options = { weekday: 'short', day: '2-digit', month: '2-digit' };
26
+ const dateStr = evDate.toLocaleDateString('de-DE', options);
27
+
28
+ upcomingHtml += `
29
+ <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.05);">
30
+ <div style="width: 4px; height: 24px; background: ${color}; border-radius: 2px;"></div>
31
+ <div style="flex-grow: 1;">
32
+ <div style="font-size: 14px; font-weight: 600; color: #fff;">${ev.title.replace(/</g, '&lt;')}</div>
33
+ <div style="font-size: 12px; color: rgba(255,255,255,0.5);">${dateStr}${ev.time ? ' • ' + ev.time : ''}</div>
34
+ </div>
35
+ </div>
36
+ `;
37
+ });
38
+ }
39
+
40
  this.container.innerHTML = `
41
+ <div class="hub-container fade-in" style="padding-top: calc(env(safe-area-inset-top, 20px) + 20px); min-height: 100%; padding-bottom: 32px; overflow-y: auto;">
42
  <div style="padding: 0 24px; margin-bottom: 24px;">
43
  <h1 style="font-size: 32px; font-weight: 700; margin: 0; background: linear-gradient(to right, #fff, #a0a0a0); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Campus</h1>
44
  <p style="color: var(--text-secondary); margin-top: 4px; font-size: 15px;">Deine Schul-Tools auf einen Blick</p>
 
78
  <p style="font-size: 12px; color: rgba(255,255,255,0.6); margin: 0;">Dokumente digitalisieren</p>
79
  </div>
80
  </div>
81
+
82
+ <!-- Calendar card -->
83
+ <div class="bento-card card-kalender" onclick="window.openHubSubView('Kalender')">
84
+ <div class="bento-icon-bg">
85
+ <svg viewBox="0 0 24 24" width="32" height="32" fill="currentColor" style="color: #ffcc00"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/></svg>
86
+ </div>
87
+ <div class="bento-content" style="display: flex; justify-content: space-between; align-items: flex-end;">
88
+ <div>
89
+ <h2 style="font-size: 18px; font-weight: 600; margin: 0 0 2px 0; color: #fff;">Kalender</h2>
90
+ <p style="font-size: 12px; color: rgba(255,255,255,0.6); margin: 0;">Klausuren & Hausaufgaben</p>
91
+ </div>
92
+ </div>
93
+ </div>
94
  </div>
95
 
96
+ <!-- Anstehende Termine -->
97
+ <div style="padding: 0 16px; margin-top: 16px;">
98
+ <div style="background: rgba(30,30,35,0.6); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 24px; border: 1px solid rgba(255,255,255,0.05); padding: 20px;">
99
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
100
+ <h2 style="font-size: 16px; font-weight: 600; margin: 0; color: #fff;">Anstehende Termine</h2>
101
+ <button onclick="window.openHubSubView('Kalender')" style="background: none; border: none; color: #0a84ff; font-size: 13px; font-weight: 600; cursor: pointer;">Alle ansehen</button>
102
+ </div>
103
+ <div>
104
+ ${upcomingHtml}
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
  <!-- Optional: A horizontal card below the main grid -->
110
  <div style="padding: 0 16px;">
111
  <div class="bento-card card-insights" style="margin-top: 16px; flex-direction: row; align-items: center; justify-content: space-between; padding: 20px;">
 
123
  .bento-grid {
124
  display: grid;
125
  grid-template-columns: 1.3fr 1fr;
126
+ grid-template-rows: 150px 150px 120px;
127
  gap: 16px;
128
  padding: 0 16px;
129
  }
 
154
  margin-bottom: auto;
155
  z-index: 2;
156
  }
 
 
 
157
  .bento-content {
158
  z-index: 2;
159
  position: relative;
 
173
  grid-row: 2 / 3;
174
  background: linear-gradient(160deg, rgba(255, 107, 107, 0.15) 0%, rgba(30, 30, 35, 0.8) 100%);
175
  }
176
+ .card-kalender {
177
+ grid-column: 1 / 3;
178
+ grid-row: 3 / 4;
179
+ flex-direction: row;
180
+ background: linear-gradient(160deg, rgba(255, 204, 0, 0.15) 0%, rgba(30, 30, 35, 0.8) 100%);
181
+ }
182
+ .card-kalender .bento-content {
183
+ width: 100%;
184
+ }
185
+ .card-kalender .bento-icon-bg {
186
+ margin-bottom: 0;
187
+ margin-right: 16px;
188
+ }
189
  .card-insights {
190
  background: linear-gradient(90deg, rgba(30, 30, 35, 0.8) 0%, rgba(193, 131, 240, 0.1) 100%);
191
  }