commit 973d2763f673ea0ceb71df32e0c32530b0012d76
parent 80241ab49fd0fac9457a7ce7acd3d59d83717c7a
Author: Julian Piribauer <julian.piribauer@gmail.com>
Date: Fri, 1 May 2026 21:13:11 +0000
UI polish: sidebar toggle in header, margins, daily max-width, weekends, drop categories
- Sidebar toggle button moved into monthly header (left of month/year title)
- app-body capped at max-width 1600px and centered for symmetric margins
- Daily view capped at 520px wide (fixed, monthly takes remaining space)
- Weekend cells (Sat/Sun) get a subtle background tint
- Categories removed; event form now has a free-text Notes field instead
- Event description stored in DB and shown as block tooltip in daily view
- Category modal, routes, model, and all related JS removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat:
| M | app/main.py | | | 78 | ++++++++---------------------------------------------------------------------- |
| M | app/models.py | | | 15 | +-------------- |
| M | app/schemas.py | | | 30 | ++---------------------------- |
| M | static/app.js | | | 209 | +++++++++++++++++-------------------------------------------------------------- |
| M | static/index.html | | | 74 | +++++++++++++++++++------------------------------------------------------- |
| M | static/style.css | | | 21 | ++++++++++++++++++--- |
6 files changed, 91 insertions(+), 336 deletions(-)
diff --git a/app/main.py b/app/main.py
@@ -7,10 +7,10 @@ from datetime import datetime
import os
from .database import get_db, engine
-from .models import Base, Calendar, CalendarMembership, PendingInvitation, Category, Event, EventAttendee
+from .models import Base, Calendar, CalendarMembership, PendingInvitation, Event, EventAttendee
from .schemas import (
CalendarCreate, CalendarUpdate, VisibilityUpdate, InviteRequest,
- CategoryCreate, CategoryUpdate, EventCreate, EventUpdate,
+ EventCreate, EventUpdate,
)
Base.metadata.create_all(bind=engine)
@@ -289,68 +289,6 @@ def leave_calendar(calendar_id: int, request: Request, db: Session = Depends(get
db.commit()
-# ── Categories ────────────────────────────────────────────────────────────────
-
-@app.get("/api/categories")
-def list_categories(request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
- cats = db.query(Category).filter(Category.owner_username == username).all()
- return [{"id": c.id, "name": c.name, "color": c.color, "is_visible": c.is_visible} for c in cats]
-
-
-@app.post("/api/categories", status_code=201)
-def create_category(body: CategoryCreate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
- cat = Category(name=body.name, color=body.color, owner_username=username)
- db.add(cat)
- try:
- db.commit()
- except IntegrityError:
- db.rollback()
- raise HTTPException(status_code=409, detail="Category name already exists")
- db.refresh(cat)
- return {"id": cat.id, "name": cat.name, "color": cat.color, "is_visible": cat.is_visible}
-
-
-@app.patch("/api/categories/{category_id}")
-def update_category(category_id: int, body: CategoryUpdate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
- cat = db.query(Category).filter(Category.id == category_id, Category.owner_username == username).first()
- if not cat:
- raise HTTPException(status_code=404)
- if body.name is not None:
- cat.name = body.name
- if body.color is not None:
- cat.color = body.color
- try:
- db.commit()
- except IntegrityError:
- db.rollback()
- raise HTTPException(status_code=409, detail="Category name already exists")
- return {"id": cat.id, "name": cat.name, "color": cat.color, "is_visible": cat.is_visible}
-
-
-@app.patch("/api/categories/{category_id}/visibility")
-def set_category_visibility(category_id: int, body: VisibilityUpdate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
- cat = db.query(Category).filter(Category.id == category_id, Category.owner_username == username).first()
- if not cat:
- raise HTTPException(status_code=404)
- cat.is_visible = body.is_visible
- db.commit()
- return {"is_visible": cat.is_visible}
-
-
-@app.delete("/api/categories/{category_id}", status_code=204)
-def delete_category(category_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
- cat = db.query(Category).filter(Category.id == category_id, Category.owner_username == username).first()
- if not cat:
- raise HTTPException(status_code=404)
- db.delete(cat)
- db.commit()
-
-
# ── Events ────────────────────────────────────────────────────────────────────
def _parse_dt(s: str) -> datetime:
@@ -381,7 +319,7 @@ def list_events(year: int, month: int, request: Request, db: Session = Depends(g
"title": e.title,
"start_dt": e.start_dt.isoformat(),
"end_dt": e.end_dt.isoformat(),
- "category_id": e.category_id,
+ "description": e.description,
"attendees": [a.username for a in e.attendees],
}
for e in events
@@ -402,7 +340,7 @@ def create_event(body: EventCreate, request: Request, db: Session = Depends(get_
title=body.title,
start_dt=start,
end_dt=end,
- category_id=body.category_id,
+ description=body.description,
)
db.add(ev)
db.flush()
@@ -416,7 +354,7 @@ def create_event(body: EventCreate, request: Request, db: Session = Depends(get_
"title": ev.title,
"start_dt": ev.start_dt.isoformat(),
"end_dt": ev.end_dt.isoformat(),
- "category_id": ev.category_id,
+ "description": ev.description,
"attendees": [a.username for a in ev.attendees],
}
@@ -435,8 +373,8 @@ def update_event(event_id: int, body: EventUpdate, request: Request, db: Session
ev.start_dt = _parse_dt(body.start_dt)
if body.end_dt is not None:
ev.end_dt = _parse_dt(body.end_dt)
- if body.category_id is not None:
- ev.category_id = body.category_id
+ if body.description is not None:
+ ev.description = body.description
if body.attendee_usernames is not None:
for a in ev.attendees:
db.delete(a)
@@ -451,7 +389,7 @@ def update_event(event_id: int, body: EventUpdate, request: Request, db: Session
"title": ev.title,
"start_dt": ev.start_dt.isoformat(),
"end_dt": ev.end_dt.isoformat(),
- "category_id": ev.category_id,
+ "description": ev.description,
"attendees": [a.username for a in ev.attendees],
}
diff --git a/app/models.py b/app/models.py
@@ -45,18 +45,6 @@ class PendingInvitation(Base):
calendar = relationship("Calendar", back_populates="pending_invitations")
-class Category(Base):
- __tablename__ = "categories"
-
- id = Column(Integer, primary_key=True)
- name = Column(String, nullable=False)
- color = Column(String, nullable=False, default="#EBCB8B")
- owner_username = Column(String, nullable=False)
- is_visible = Column(Boolean, default=True, nullable=False)
-
- __table_args__ = (UniqueConstraint("owner_username", "name"),)
-
-
class Event(Base):
__tablename__ = "events"
@@ -65,10 +53,9 @@ class Event(Base):
title = Column(String, nullable=False)
start_dt = Column(DateTime, nullable=False)
end_dt = Column(DateTime, nullable=False)
- category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
+ description = Column(String, nullable=True)
calendar = relationship("Calendar")
- category = relationship("Category")
attendees = relationship(
"EventAttendee", back_populates="event", cascade="all, delete-orphan"
)
diff --git a/app/schemas.py b/app/schemas.py
@@ -35,38 +35,12 @@ class InviteRequest(BaseModel):
email: str
-def _hex(v: str | None) -> str | None:
- if v is not None and not re.match(r"^#[0-9a-fA-F]{6}$", v):
- raise ValueError("invalid hex color")
- return v
-
-
-class CategoryCreate(BaseModel):
- name: str
- color: str = "#EBCB8B"
-
- @field_validator("color")
- @classmethod
- def valid_color(cls, v: str) -> str:
- return _hex(v) # type: ignore[return-value]
-
-
-class CategoryUpdate(BaseModel):
- name: str | None = None
- color: str | None = None
-
- @field_validator("color")
- @classmethod
- def valid_color(cls, v: str | None) -> str | None:
- return _hex(v)
-
-
class EventCreate(BaseModel):
calendar_id: int
title: str
start_dt: str
end_dt: str
- category_id: int | None = None
+ description: str | None = None
attendee_usernames: list[str] = []
@@ -74,5 +48,5 @@ class EventUpdate(BaseModel):
title: str | None = None
start_dt: str | None = None
end_dt: str | None = None
- category_id: int | None = None
+ description: str | None = None
attendee_usernames: list[str] | None = None
diff --git a/static/app.js b/static/app.js
@@ -5,7 +5,6 @@ const state = {
username: '',
email: '',
calendars: [],
- categories: [],
events: {}, // keyed "YYYY-MM-DD" → array of event objects
year: new Date().getFullYear(),
month: new Date().getMonth(), // 0-based
@@ -44,7 +43,7 @@ async function init() {
state.email = me.email;
document.getElementById('username-display').textContent = me.username;
- await Promise.all([loadCalendars(), loadCategories()]);
+ await loadCalendars();
await fetchEvents();
renderMonthly();
@@ -60,11 +59,6 @@ async function loadCalendars() {
renderSidebar();
}
-async function loadCategories() {
- state.categories = await api('GET', '/api/categories');
- renderCategories();
-}
-
async function fetchEvents() {
const evs = await api('GET', `/api/events?year=${state.year}&month=${state.month + 1}`);
indexEvents(evs);
@@ -98,9 +92,7 @@ document.getElementById('btn-sidebar-toggle').addEventListener('click', () => {
function renderSidebar() {
const list = document.getElementById('calendar-list');
list.innerHTML = '';
- state.calendars.forEach(cal => {
- list.appendChild(makeCalendarItem(cal));
- });
+ state.calendars.forEach(cal => list.appendChild(makeCalendarItem(cal)));
}
function makeCalendarItem(cal) {
@@ -122,7 +114,7 @@ function makeCalendarItem(cal) {
toggle.addEventListener('click', e => { e.stopPropagation(); toggleCalVisibility(cal); });
item.append(dot, name, toggle);
- item.addEventListener('contextmenu', e => { e.preventDefault(); showContextMenu(e, cal, 'calendar'); });
+ item.addEventListener('contextmenu', e => { e.preventDefault(); showContextMenu(e, cal); });
return item;
}
@@ -134,64 +126,20 @@ async function toggleCalVisibility(cal) {
if (state.selectedDay) renderDailyEvents();
}
-// ── Sidebar: Categories ───────────────────────────────────────────────────────
-function renderCategories() {
- const list = document.getElementById('category-list');
- list.innerHTML = '';
- state.categories.forEach(cat => {
- list.appendChild(makeCategoryItem(cat));
- });
-}
-
-function makeCategoryItem(cat) {
- const item = document.createElement('div');
- item.className = 'calendar-item';
- item.dataset.id = cat.id;
-
- const dot = document.createElement('span');
- dot.className = 'cal-dot';
- dot.style.background = cat.color;
-
- const name = document.createElement('span');
- name.className = 'cal-name';
- name.textContent = cat.name;
-
- const toggle = document.createElement('button');
- toggle.className = 'cal-toggle' + (cat.is_visible ? ' visible' : '');
- toggle.title = cat.is_visible ? 'Hide' : 'Show';
- toggle.addEventListener('click', e => { e.stopPropagation(); toggleCatVisibility(cat); });
-
- item.append(dot, name, toggle);
- item.addEventListener('contextmenu', e => { e.preventDefault(); showContextMenu(e, cat, 'category'); });
- return item;
-}
-
-async function toggleCatVisibility(cat) {
- await api('PATCH', `/api/categories/${cat.id}/visibility`, { is_visible: !cat.is_visible });
- cat.is_visible = !cat.is_visible;
- renderCategories();
- if (state.selectedDay) renderDailyEvents();
-}
-
// ── Context menu ──────────────────────────────────────────────────────────────
const ctxMenu = document.getElementById('ctx-menu');
-function showContextMenu(e, item, type) {
+function showContextMenu(e, cal) {
ctxMenu.innerHTML = '';
- if (type === 'calendar') {
- if (item.is_owner && !item.is_personal) {
- addCtxItem('Edit', () => openCalModal('edit', item));
- addCtxItem('Invite', () => openCalModal('invite', item));
- addCtxItem('Delete', () => deleteCalendar(item), true);
- } else if (!item.is_personal) {
- addCtxItem('Leave calendar', () => leaveCalendar(item), true);
- } else {
- addCtxItem('Edit name / color', () => openCalModal('edit', item));
- }
+ if (cal.is_owner && !cal.is_personal) {
+ addCtxItem('Edit', () => openCalModal('edit', cal));
+ addCtxItem('Invite', () => openCalModal('invite', cal));
+ addCtxItem('Delete', () => deleteCalendar(cal), true);
+ } else if (!cal.is_personal) {
+ addCtxItem('Leave calendar', () => leaveCalendar(cal), true);
} else {
- addCtxItem('Edit', () => openCategoryModal(item));
- addCtxItem('Delete', () => deleteCategoryItem(item), true);
+ addCtxItem('Edit name / color', () => openCalModal('edit', cal));
}
if (!ctxMenu.children.length) return;
@@ -214,7 +162,7 @@ function closeCtxMenu() { ctxMenu.classList.remove('open'); }
document.addEventListener('click', closeCtxMenu);
document.addEventListener('keydown', e => {
- if (e.key === 'Escape') { closeCtxMenu(); closeCalModal(); closeEventModal(); closeCategoryModal(); closeDailyView(); }
+ if (e.key === 'Escape') { closeCtxMenu(); closeCalModal(); closeEventModal(); closeDailyView(); }
});
// ── Monthly view ──────────────────────────────────────────────────────────────
@@ -226,7 +174,7 @@ function renderMonthly() {
const firstDay = new Date(state.year, state.month, 1);
let startDow = firstDay.getDay();
- startDow = startDow === 0 ? 6 : startDow - 1;
+ startDow = startDow === 0 ? 6 : startDow - 1; // Mon=0 … Sun=6
const daysInMonth = new Date(state.year, state.month + 1, 0).getDate();
const daysInPrev = new Date(state.year, state.month, 0).getDate();
@@ -243,12 +191,17 @@ function renderMonthly() {
day = i - startDow + 1;
}
+ const col = i % 7; // 0=Mon … 6=Sun
+ const isWeekend = col >= 5;
+
const cell = document.createElement('div');
- cell.className = 'day-cell' + (!isCurrent ? ' other-month' : '');
+ cell.className = 'day-cell';
+ if (!isCurrent) cell.classList.add('other-month');
+ if (isWeekend) cell.classList.add('weekend');
const isToday = isCurrent && day === today.getDate() &&
state.month === today.getMonth() && state.year === today.getFullYear();
- if (isToday) cell.classList.add('today');
+ if (isToday) cell.classList.add('today');
if (isCurrent && state.selectedDay === day) cell.classList.add('selected');
const num = document.createElement('div');
@@ -256,7 +209,7 @@ function renderMonthly() {
num.textContent = day;
cell.appendChild(num);
- // Event dots
+ // Event dots for current-month days
if (isCurrent) {
const k = `${state.year}-${String(state.month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
const dayEvs = (state.events[k] || []).filter(ev => {
@@ -281,9 +234,9 @@ function renderMonthly() {
}
cell.appendChild(dots);
}
+ cell.addEventListener('click', () => selectDay(day));
}
- if (isCurrent) cell.addEventListener('click', () => selectDay(day));
grid.appendChild(cell);
}
}
@@ -313,7 +266,7 @@ document.getElementById('next-month').addEventListener('click', nextMonth);
// ── Daily view ────────────────────────────────────────────────────────────────
function openDailyView(day) {
- const date = new Date(state.year, state.month, day);
+ const date = new Date(state.year, state.month, day);
document.getElementById('daily-title').textContent = date.toLocaleDateString('en-GB', {
weekday: 'long', day: 'numeric', month: 'long',
});
@@ -375,12 +328,7 @@ function renderDailyEvents() {
const k = `${state.year}-${String(state.month+1).padStart(2,'0')}-${String(state.selectedDay).padStart(2,'0')}`;
const dayEvs = (state.events[k] || []).filter(ev => {
const cal = state.calendars.find(c => c.id === ev.calendar_id);
- if (!cal || !cal.is_visible) return false;
- if (ev.category_id) {
- const cat = state.categories.find(c => c.id === ev.category_id);
- if (cat && !cat.is_visible) return false;
- }
- return true;
+ return cal && cal.is_visible;
});
const dayStart = new Date(state.year, state.month, state.selectedDay, 6, 0);
@@ -389,12 +337,12 @@ function renderDailyEvents() {
dayEvs.forEach(ev => {
const evStart = new Date(ev.start_dt);
const evEnd = new Date(ev.end_dt);
- const clamped_start = evStart < dayStart ? dayStart : evStart;
- const clamped_end = evEnd > dayEnd ? dayEnd : evEnd;
- if (clamped_start >= clamped_end) return;
+ const cs = evStart < dayStart ? dayStart : evStart;
+ const ce = evEnd > dayEnd ? dayEnd : evEnd;
+ if (cs >= ce) return;
- const startMin = (clamped_start.getHours() - 6) * 60 + clamped_start.getMinutes();
- const durMin = (clamped_end - clamped_start) / 60000;
+ const startMin = (cs.getHours() - 6) * 60 + cs.getMinutes();
+ const durMin = (ce - cs) / 60000;
const cal = state.calendars.find(c => c.id === ev.calendar_id);
const color = cal ? cal.color : '#88C0D0';
@@ -406,7 +354,7 @@ function renderDailyEvents() {
block.style.borderLeftColor = color;
block.style.color = color;
block.textContent = ev.title;
- block.title = ev.title;
+ block.title = ev.description ? `${ev.title}\n${ev.description}` : ev.title;
block.addEventListener('click', e => { e.stopPropagation(); openEventModal(null, ev); });
overlay.appendChild(block);
});
@@ -419,10 +367,10 @@ document.getElementById('btn-add-event').addEventListener('click', () => {
// ── Calendar modal (create / edit / invite) ───────────────────────────────────
const calModalOverlay = document.getElementById('modal-overlay');
const calModal = document.getElementById('modal');
-let calModalMode = 'create';
+let calModalMode = 'create';
let calModalTarget = null;
-let selectedColor = COLORS[0];
-let inviteEmails = [''];
+let selectedColor = COLORS[0];
+let inviteEmails = [''];
function openCalModal(mode, target = null) {
calModalMode = mode;
@@ -508,7 +456,9 @@ function renderInviteRows(list) {
const rem = document.createElement('button');
rem.className = 'btn-icon danger';
rem.innerHTML = svgMinus();
- rem.addEventListener('click', () => { if (inviteEmails.length > 1) { inviteEmails.splice(i, 1); renderInviteRows(list); } });
+ rem.addEventListener('click', () => {
+ if (inviteEmails.length > 1) { inviteEmails.splice(i, 1); renderInviteRows(list); }
+ });
row.append(inp, add, rem);
list.appendChild(row);
@@ -549,66 +499,6 @@ async function leaveCalendar(cal) {
document.getElementById('btn-new-cal').addEventListener('click', () => openCalModal('create'));
-// ── Category modal ────────────────────────────────────────────────────────────
-const catModalOverlay = document.getElementById('cat-modal-overlay');
-let catModalTarget = null;
-let catSelectedColor = COLORS[4]; // green default
-
-function openCategoryModal(cat = null) {
- catModalTarget = cat;
- catSelectedColor = cat ? cat.color : COLORS[4];
- renderCategoryModal();
- catModalOverlay.classList.add('open');
-}
-
-function closeCategoryModal() { catModalOverlay.classList.remove('open'); }
-
-catModalOverlay.addEventListener('click', e => { if (e.target === catModalOverlay) closeCategoryModal(); });
-document.getElementById('cat-modal-cancel').addEventListener('click', closeCategoryModal);
-document.getElementById('cat-modal-submit').addEventListener('click', submitCategoryModal);
-
-function renderCategoryModal() {
- const modal = document.getElementById('cat-modal');
- modal.querySelector('h2').textContent = catModalTarget ? 'Edit category' : 'New category';
- document.getElementById('cat-name-input').value = catModalTarget ? catModalTarget.name : '';
-
- const swatches = document.getElementById('cat-color-swatches');
- swatches.innerHTML = '';
- COLORS.forEach(c => {
- const sw = document.createElement('div');
- sw.className = 'color-swatch' + (c === catSelectedColor ? ' selected' : '');
- sw.style.background = c;
- sw.addEventListener('click', () => {
- catSelectedColor = c;
- swatches.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
- sw.classList.add('selected');
- });
- swatches.appendChild(sw);
- });
-}
-
-async function submitCategoryModal() {
- const name = document.getElementById('cat-name-input').value.trim();
- if (!name) return alert('Please enter a name.');
- try {
- if (catModalTarget) {
- await api('PATCH', `/api/categories/${catModalTarget.id}`, { name, color: catSelectedColor });
- } else {
- await api('POST', '/api/categories', { name, color: catSelectedColor });
- }
- closeCategoryModal();
- await loadCategories();
- } catch (err) { alert(err.message); }
-}
-
-async function deleteCategoryItem(cat) {
- if (!confirm(`Delete category "${cat.name}"?`)) return;
- await api('DELETE', `/api/categories/${cat.id}`);
- await loadCategories();
-}
-
-document.getElementById('btn-new-cat').addEventListener('click', () => openCategoryModal());
-
// ── Event modal ───────────────────────────────────────────────────────────────
const evModalOverlay = document.getElementById('event-modal-overlay');
let editingEvent = null;
@@ -630,7 +520,7 @@ function openEventModal(prefillDate, existingEvent = null) {
});
// Dates & times
- const d = existingEvent ? new Date(existingEvent.start_dt) : (prefillDate || new Date());
+ const d = existingEvent ? new Date(existingEvent.start_dt) : (prefillDate || new Date());
const eD = existingEvent ? new Date(existingEvent.end_dt)
: new Date(d.getTime() + 60 * 60 * 1000);
@@ -639,24 +529,12 @@ function openEventModal(prefillDate, existingEvent = null) {
document.getElementById('ev-end-date').value = localDateStr(eD);
document.getElementById('ev-end-time').value = localTimeStr(eD);
- // Title
- document.getElementById('ev-title').value = existingEvent ? existingEvent.title : '';
-
- // Category select
- const catSel = document.getElementById('ev-category');
- catSel.innerHTML = '<option value="">— None —</option>';
- state.categories.forEach(cat => {
- const opt = document.createElement('option');
- opt.value = cat.id;
- opt.textContent = cat.name;
- if (existingEvent && existingEvent.category_id === cat.id) opt.selected = true;
- catSel.appendChild(opt);
- });
+ document.getElementById('ev-title').value = existingEvent ? existingEvent.title : '';
+ document.getElementById('ev-description').value = existingEvent ? (existingEvent.description || '') : '';
updateEventAttendees(existingEvent);
calSel.addEventListener('change', () => updateEventAttendees(existingEvent));
- // Delete button visibility
document.getElementById('ev-delete-btn').style.display = existingEvent ? 'inline-flex' : 'none';
evModalOverlay.classList.add('open');
@@ -698,9 +576,9 @@ document.getElementById('event-modal-submit').addEventListener('click', async ()
const startT = document.getElementById('ev-start-time').value;
const endD = document.getElementById('ev-end-date').value;
const endT = document.getElementById('ev-end-time').value;
- const catId = document.getElementById('ev-category').value || null;
+ const desc = document.getElementById('ev-description').value.trim() || null;
- if (!title) return alert('Please enter a title.');
+ if (!title) return alert('Please enter a title.');
if (!startD || !startT || !endD || !endT) return alert('Please fill in start and end times.');
const startDt = `${startD}T${startT}:00`;
@@ -715,7 +593,7 @@ document.getElementById('event-modal-submit').addEventListener('click', async ()
title,
start_dt: startDt,
end_dt: endDt,
- category_id: catId ? parseInt(catId) : null,
+ description: desc,
attendee_usernames: attendees,
};
@@ -733,8 +611,7 @@ document.getElementById('event-modal-submit').addEventListener('click', async ()
});
document.getElementById('ev-delete-btn').addEventListener('click', async () => {
- if (!editingEvent) return;
- if (!confirm(`Delete "${editingEvent.title}"?`)) return;
+ if (!editingEvent || !confirm(`Delete "${editingEvent.title}"?`)) return;
await api('DELETE', `/api/events/${editingEvent.id}`);
closeEventModal();
await fetchEvents();
diff --git a/static/index.html b/static/index.html
@@ -14,11 +14,6 @@
<nav class="topbar">
<div class="topbar-inner">
<div class="topbar-left">
- <button class="btn-sidebar-toggle" id="btn-sidebar-toggle" title="Toggle sidebar">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M3 6h18M3 12h18M3 18h18" stroke-linecap="round"/>
- </svg>
- </button>
<a href="/" class="topbar-name">Calendar</a>
<a href="https://hub.piribauer.ch" class="topbar-back">Hub</a>
</div>
@@ -39,35 +34,16 @@
<!-- Sidebar -->
<aside class="sidebar">
-
- <div class="sidebar-section">
- <div class="sidebar-header">Calendars</div>
- <div class="calendar-list" id="calendar-list"></div>
- <div class="sidebar-actions">
- <button class="btn-new-cal" id="btn-new-cal">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M12 5v14M5 12h14" stroke-linecap="round"/>
- </svg>
- New calendar
- </button>
- </div>
- </div>
-
- <div class="sidebar-divider"></div>
-
- <div class="sidebar-section">
- <div class="sidebar-header">Categories</div>
- <div class="calendar-list" id="category-list"></div>
- <div class="sidebar-actions">
- <button class="btn-new-cal" id="btn-new-cat">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M12 5v14M5 12h14" stroke-linecap="round"/>
- </svg>
- New category
- </button>
- </div>
+ <div class="sidebar-header">Calendars</div>
+ <div class="calendar-list" id="calendar-list"></div>
+ <div class="sidebar-actions">
+ <button class="btn-new-cal" id="btn-new-cal">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M12 5v14M5 12h14" stroke-linecap="round"/>
+ </svg>
+ New calendar
+ </button>
</div>
-
</aside>
<!-- Calendar main -->
@@ -76,7 +52,14 @@
<!-- Monthly view -->
<section class="monthly-view">
<div class="monthly-header">
- <h2 class="monthly-title" id="month-title"></h2>
+ <div class="monthly-header-left">
+ <button class="btn-sidebar-toggle" id="btn-sidebar-toggle" title="Toggle sidebar">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M3 6h18M3 12h18M3 18h18" stroke-linecap="round"/>
+ </svg>
+ </button>
+ <h2 class="monthly-title" id="month-title"></h2>
+ </div>
<div class="nav-btns">
<button class="nav-btn" id="prev-month" title="Previous month">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -138,25 +121,6 @@
</div>
</div>
- <!-- Category modal -->
- <div class="modal-overlay" id="cat-modal-overlay">
- <div class="modal" id="cat-modal">
- <h2>New category</h2>
- <div class="form-group">
- <label class="form-label">Name</label>
- <input class="form-input" id="cat-name-input" type="text" placeholder="Category name">
- </div>
- <div class="form-group">
- <label class="form-label">Color</label>
- <div class="color-swatches" id="cat-color-swatches"></div>
- </div>
- <div class="modal-actions">
- <button class="btn" id="cat-modal-cancel">Cancel</button>
- <button class="btn btn-primary" id="cat-modal-submit">Save</button>
- </div>
- </div>
- </div>
-
<!-- Event modal -->
<div class="modal-overlay" id="event-modal-overlay">
<div class="modal event-modal" id="event-modal">
@@ -195,8 +159,8 @@
</div>
<div class="form-group">
- <label class="form-label">Category</label>
- <select class="form-input" id="ev-category"></select>
+ <label class="form-label">Notes</label>
+ <textarea class="form-input" id="ev-description" placeholder="Optional notes…"></textarea>
</div>
<div class="form-group" id="ev-attendees-group" style="display:none">
diff --git a/static/style.css b/static/style.css
@@ -78,6 +78,9 @@ body {
flex: 1;
min-height: 0;
overflow: hidden;
+ max-width: 1600px;
+ width: 100%;
+ margin: 0 auto;
}
/* ── Sidebar ── */
@@ -199,8 +202,7 @@ body {
overflow: hidden;
}
.daily-open .monthly-view {
- flex: 0 0 400px;
- max-width: 400px;
+ flex: 1 1 auto;
}
.monthly-header {
display: flex;
@@ -209,6 +211,11 @@ body {
padding: 1.25rem 1.5rem 1rem;
flex-shrink: 0;
}
+.monthly-header-left {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
.monthly-title {
font-family: 'EB Garamond', Georgia, serif;
font-size: 1.5rem;
@@ -275,6 +282,7 @@ body {
border-color: var(--polar4);
}
.day-cell.other-month { opacity: 0.3; }
+.day-cell.weekend { background: rgba(255,255,255,0.025); }
.day-cell.today .day-number {
background: var(--lightblue);
color: var(--polar1);
@@ -315,7 +323,8 @@ body {
border-color 0.35s;
}
.daily-open .daily-view {
- flex: 1 1 auto;
+ flex: 0 0 520px;
+ max-width: 520px;
opacity: 1;
border-color: var(--polar3);
}
@@ -488,6 +497,12 @@ body {
.color-swatch:hover { transform: scale(1.1); }
.color-swatch.selected { border-color: var(--snow3); }
+textarea.form-input {
+ resize: vertical;
+ min-height: 72px;
+ line-height: 1.5;
+}
+
.invite-list { display: flex; flex-direction: column; gap: 0.4rem; }
.invite-row { display: flex; gap: 0.4rem; }
.invite-row .form-input { flex: 1; }