commit b4aad6e242ad174350f6f1be6871080031861601
parent 5d26999e2c78f88c0fec74e7d0acc7d62e060831
Author: Julian Piribauer <julian.piribauer@gmail.com>
Date: Sun, 10 May 2026 13:27:54 +0000
Adding event preview in side-bar
Diffstat:
5 files changed, 215 insertions(+), 4 deletions(-)
diff --git a/app/main.py b/app/main.py
@@ -4,7 +4,7 @@ from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from sqlalchemy import text
-from datetime import datetime
+from datetime import datetime, timedelta
import os
from .database import get_db, engine
@@ -382,6 +382,37 @@ def list_events(year: int, month: int, request: Request, db: Session = Depends(g
]
+@app.get("/api/events/upcoming")
+def list_upcoming_events(request: Request, days: int = 90, db: Session = Depends(get_db)):
+ username, _, _dn = _get_identity(request)
+ memberships = db.query(CalendarMembership).filter(CalendarMembership.username == username).all()
+ cal_ids = [m.calendar_id for m in memberships]
+
+ now = datetime.now()
+ today_start = datetime(now.year, now.month, now.day)
+ cutoff = today_start + timedelta(days=days)
+
+ events = (
+ db.query(Event)
+ .filter(Event.calendar_id.in_(cal_ids), Event.end_dt >= today_start, Event.start_dt < cutoff)
+ .order_by(Event.start_dt)
+ .all()
+ )
+ return [
+ {
+ "id": e.id,
+ "calendar_id": e.calendar_id,
+ "title": e.title,
+ "start_dt": e.start_dt.isoformat(),
+ "end_dt": e.end_dt.isoformat(),
+ "description": e.description,
+ "all_day": e.all_day,
+ "attendees": [a.username for a in e.attendees],
+ }
+ for e in events
+ ]
+
+
@app.post("/api/events", status_code=201)
def create_event(body: EventCreate, request: Request, db: Session = Depends(get_db)):
username, _, _dn = _get_identity(request)
diff --git a/static/app.js b/static/app.js
@@ -6,6 +6,7 @@ const state = {
email: '',
calendars: [],
events: {}, // keyed "YYYY-MM-DD" → array of event objects
+ upcomingEvents: [],
year: new Date().getFullYear(),
month: new Date().getMonth(), // 0-based
selectedDay: null,
@@ -45,7 +46,7 @@ async function init() {
document.getElementById('username-display').textContent = me.display_name || me.username;
await loadCalendars();
- await fetchEvents();
+ await Promise.all([fetchEvents(), fetchUpcomingEvents()]);
renderMonthly();
// Collapse sidebar on mobile by default
@@ -65,6 +66,11 @@ async function fetchEvents() {
indexEvents(evs);
}
+async function fetchUpcomingEvents() {
+ state.upcomingEvents = await api('GET', '/api/events/upcoming');
+ renderUpcomingEvents();
+}
+
function indexEvents(arr) {
state.events = {};
arr.forEach(ev => {
@@ -89,6 +95,115 @@ document.getElementById('btn-sidebar-toggle').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('collapsed');
});
+// ── Sidebar: Upcoming events ──────────────────────────────────────────────────
+function renderUpcomingEvents() {
+ const section = document.getElementById('upcoming-section');
+ section.innerHTML = '';
+
+ const now = new Date();
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const in7Days = new Date(todayStart);
+ in7Days.setDate(in7Days.getDate() + 7);
+
+ const visibleEvs = state.upcomingEvents.filter(ev => {
+ const cal = state.calendars.find(c => c.id === ev.calendar_id);
+ return cal && cal.is_visible;
+ });
+
+ const near = visibleEvs.filter(ev => new Date(ev.start_dt) < in7Days);
+ const later = visibleEvs.filter(ev => new Date(ev.start_dt) >= in7Days);
+
+ section.appendChild(makeUpcomingSectionHeader(t('upcoming-7days')));
+ if (near.length === 0) {
+ section.appendChild(makeUpcomingEmpty());
+ } else {
+ near.forEach(ev => section.appendChild(makeUpcomingItem(ev)));
+ }
+
+ const divider = document.createElement('div');
+ divider.className = 'sidebar-divider';
+ section.appendChild(divider);
+
+ section.appendChild(makeUpcomingSectionHeader(t('upcoming-later')));
+ if (later.length === 0) {
+ section.appendChild(makeUpcomingEmpty());
+ } else {
+ later.forEach(ev => section.appendChild(makeUpcomingItem(ev)));
+ }
+}
+
+function makeUpcomingSectionHeader(label) {
+ const el = document.createElement('div');
+ el.className = 'upcoming-header';
+ el.textContent = label;
+ return el;
+}
+
+function makeUpcomingEmpty() {
+ const el = document.createElement('div');
+ el.className = 'upcoming-empty';
+ el.textContent = t('upcoming-empty');
+ return el;
+}
+
+function makeUpcomingItem(ev) {
+ const cal = state.calendars.find(c => c.id === ev.calendar_id);
+ const color = cal ? cal.color : '#88C0D0';
+
+ const item = document.createElement('div');
+ item.className = 'upcoming-item';
+ item.style.borderLeftColor = color;
+
+ const title = document.createElement('div');
+ title.className = 'upcoming-title';
+ title.textContent = ev.title;
+
+ const meta = document.createElement('div');
+ meta.className = 'upcoming-meta';
+ meta.textContent = formatUpcomingDate(new Date(ev.start_dt), ev.all_day);
+
+ item.append(title, meta);
+ item.addEventListener('click', () => navigateToEvent(ev));
+ return item;
+}
+
+function formatUpcomingDate(date, allDay) {
+ const now = new Date();
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const tomorrow = new Date(today);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ const eventDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+
+ let dayLabel;
+ if (eventDay.getTime() === today.getTime()) {
+ dayLabel = t('upcoming-today');
+ } else if (eventDay.getTime() === tomorrow.getTime()) {
+ dayLabel = t('upcoming-tomorrow');
+ } else {
+ dayLabel = date.toLocaleDateString(getLang() === 'de' ? 'de-DE' : 'en-GB', {
+ weekday: 'short', day: 'numeric', month: 'short',
+ });
+ }
+
+ if (allDay) return dayLabel;
+ return `${dayLabel} · ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`;
+}
+
+async function navigateToEvent(ev) {
+ const start = new Date(ev.start_dt);
+ const newYear = start.getFullYear();
+ const newMonth = start.getMonth();
+ const newDay = start.getDate();
+ state.selectedDay = newDay;
+ if (newYear !== state.year || newMonth !== state.month) {
+ state.year = newYear;
+ state.month = newMonth;
+ await fetchEvents();
+ }
+ renderMonthly();
+ openDailyView(newDay);
+}
+
// ── Sidebar: Calendars ────────────────────────────────────────────────────────
function renderSidebar() {
const list = document.getElementById('calendar-list');
@@ -123,6 +238,7 @@ async function toggleCalVisibility(cal) {
await api('PATCH', `/api/calendars/${cal.id}/visibility`, { is_visible: !cal.is_visible });
cal.is_visible = !cal.is_visible;
renderSidebar();
+ renderUpcomingEvents();
renderMonthly();
if (state.selectedDay) renderDailyEvents();
}
@@ -733,7 +849,7 @@ document.getElementById('event-modal-submit').addEventListener('click', async ()
await api('POST', '/api/events', payload);
}
closeEventModal();
- await fetchEvents();
+ await Promise.all([fetchEvents(), fetchUpcomingEvents()]);
renderMonthly();
if (state.selectedDay) renderDailyEvents();
} catch (err) { alert(err.message); }
@@ -743,7 +859,7 @@ document.getElementById('ev-delete-btn').addEventListener('click', async () => {
if (!editingEvent || !confirm(`Delete "${editingEvent.title}"?`)) return;
await api('DELETE', `/api/events/${editingEvent.id}`);
closeEventModal();
- await fetchEvents();
+ await Promise.all([fetchEvents(), fetchUpcomingEvents()]);
renderMonthly();
if (state.selectedDay) renderDailyEvents();
});
@@ -769,6 +885,7 @@ function svgMinus() {
document.addEventListener('langchange', () => {
renderMonthly();
renderSidebar();
+ renderUpcomingEvents();
if (state.selectedDay) {
const date = new Date(state.year, state.month, state.selectedDay);
document.getElementById('daily-title').textContent = date.toLocaleDateString(
diff --git a/static/i18n.js b/static/i18n.js
@@ -7,6 +7,11 @@
// sidebar
'sidebar-calendars': 'Calendars',
'new-calendar': 'New calendar',
+ 'upcoming-7days': 'Next 7 days',
+ 'upcoming-later': 'Later',
+ 'upcoming-today': 'Today',
+ 'upcoming-tomorrow': 'Tomorrow',
+ 'upcoming-empty': 'Nothing scheduled',
// monthly view
'toggle-sidebar': 'Toggle sidebar',
'prev-month': 'Previous month',
@@ -69,6 +74,11 @@
'sign-out': 'Abmelden',
'sidebar-calendars': 'Kalender',
'new-calendar': 'Neuer Kalender',
+ 'upcoming-7days': 'Nächste 7 Tage',
+ 'upcoming-later': 'Später',
+ 'upcoming-today': 'Heute',
+ 'upcoming-tomorrow': 'Morgen',
+ 'upcoming-empty': 'Nichts geplant',
'toggle-sidebar': 'Seitenleiste umschalten',
'prev-month': 'Vorheriger Monat',
'next-month': 'Nächster Monat',
diff --git a/static/index.html b/static/index.html
@@ -39,6 +39,8 @@
<!-- Sidebar -->
<aside class="sidebar">
+ <div id="upcoming-section" class="upcoming-section"></div>
+ <div class="sidebar-divider"></div>
<div class="sidebar-header" data-i18n="sidebar-calendars">Calendars</div>
<div class="calendar-list" id="calendar-list"></div>
<div class="sidebar-actions">
diff --git a/static/style.css b/static/style.css
@@ -789,6 +789,57 @@ textarea.form-input {
flex-shrink: 0;
}
+/* ── Upcoming events ── */
+.upcoming-section {
+ flex-shrink: 0;
+ max-height: 260px;
+ overflow-y: auto;
+ padding-bottom: 0.15rem;
+}
+.upcoming-header {
+ padding: 1.1rem 1rem 0.5rem;
+ font-size: 0.68rem;
+ font-weight: 500;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--snow1);
+ opacity: 0.45;
+}
+.upcoming-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ padding: 0.35rem 0.6rem 0.35rem 0.65rem;
+ margin: 1px 0.5rem;
+ border-radius: 5px;
+ border-left: 3px solid transparent;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+.upcoming-item:hover { background: var(--polar3); }
+.upcoming-title {
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: var(--snow2);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.3;
+}
+.upcoming-meta {
+ font-size: 0.68rem;
+ color: var(--snow1);
+ opacity: 0.5;
+ line-height: 1.2;
+}
+.upcoming-empty {
+ padding: 0.2rem 1rem 0.6rem;
+ font-size: 0.75rem;
+ color: var(--snow1);
+ opacity: 0.3;
+ font-style: italic;
+}
+
/* ── Daily header with add-event button ── */
.daily-header-right {
display: flex;