big update i guess

This commit is contained in:
2026-03-26 18:55:15 +01:00
parent 1bbabd6c4d
commit 3f3609c944
12 changed files with 511 additions and 104 deletions

View File

@@ -164,12 +164,12 @@ function bindLoginForm() {
function loadAvatarImage(avatarEl, username) {
const img = new Image();
img.onload = () => {
avatarEl.textContent = '';
img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%';
avatarEl.innerHTML = '';
img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
avatarEl.appendChild(img);
};
img.onerror = () => {
// Fallback to letter
avatarEl.innerHTML = '';
avatarEl.textContent = (username || '?')[0].toUpperCase();
};
img.src = `/api/profile/avatar?t=${Date.now()}`;

View File

@@ -1,9 +1,12 @@
import { api } from './api.js';
import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey } from './utils.js';
import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey, dayOfWeek, weekStart } from './utils.js';
import { renderMonth } from './views/month.js';
import { renderWeek } from './views/week.js';
import { renderAgenda } from './views/agenda.js';
// week start day global (loaded from settings)
let weekStartDay = 'monday';
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
@@ -29,6 +32,7 @@ export async function initCalendar() {
state.accounts = accounts;
state.currentView = settings.default_view || 'month';
state.dimPast = settings.dim_past_events;
weekStartDay = settings.week_start_day || 'monday';
applyTheme(settings);
updateViewButtons();
@@ -65,13 +69,11 @@ function getViewRange() {
if (state.currentView === 'month') {
start = new Date(d.getFullYear(), d.getMonth(), 1);
start.setDate(start.getDate() - start.getDay() - 1);
start.setDate(start.getDate() - dayOfWeek(start, weekStartDay) - 1);
end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
end.setDate(end.getDate() + (6 - end.getDay()) + 1);
end.setDate(end.getDate() + (6 - dayOfWeek(end, weekStartDay)) + 1);
} else if (state.currentView === 'week') {
start = new Date(d);
start.setDate(d.getDate() - d.getDay());
start.setHours(0, 0, 0, 0);
start = weekStart(d, weekStartDay);
end = new Date(start);
end.setDate(start.getDate() + 7);
} else if (state.currentView === 'day') {
@@ -96,7 +98,8 @@ function renderView() {
if (state.currentView === 'month') {
renderMonth(container, state.currentDate, evs,
date => { state.currentDate = date; state.currentView = 'day'; updateViewButtons(); fetchAndRender(); },
showEventPopup
showEventPopup,
weekStartDay
);
} else if (state.currentView === 'week') {
renderWeek(container, state.currentDate, evs,
@@ -104,13 +107,16 @@ function renderView() {
if (switchDay) { state.currentDate = date; state.currentView = 'day'; updateViewButtons(); fetchAndRender(); }
else openNewEventModal(date);
},
showEventPopup
showEventPopup,
false,
weekStartDay
);
} else if (state.currentView === 'day') {
renderWeek(container, state.currentDate, evs,
(date, switchDay) => { if (!switchDay) openNewEventModal(date); },
showEventPopup,
true
true,
weekStartDay
);
} else {
renderAgenda(container, state.currentDate, evs, showEventPopup);
@@ -133,14 +139,13 @@ function updateTitle() {
if (state.currentView === 'month') {
title = `${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
} else if (state.currentView === 'week') {
const sun = new Date(d);
sun.setDate(d.getDate() - d.getDay());
const sat = new Date(sun);
sat.setDate(sun.getDate() + 6);
const sameMonth = sun.getMonth() === sat.getMonth();
const mon = weekStart(d, weekStartDay);
const sun = new Date(mon);
sun.setDate(mon.getDate() + 6);
const sameMonth = mon.getMonth() === sun.getMonth();
title = sameMonth
? `${sun.getDate()}. ${sat.getDate()}. ${MONTHS[sat.getMonth()]} ${sat.getFullYear()}`
: `${sun.getDate()}. ${MONTHS[sun.getMonth()]} ${sat.getDate()}. ${MONTHS[sat.getMonth()]} ${sat.getFullYear()}`;
? `${mon.getDate()}. ${sun.getDate()}. ${MONTHS[sun.getMonth()]} ${sun.getFullYear()}`
: `${mon.getDate()}. ${MONTHS[mon.getMonth()]} ${sun.getDate()}. ${MONTHS[sun.getMonth()]} ${sun.getFullYear()}`;
} else if (state.currentView === 'day') {
title = `${d.getDate()}. ${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
} else {
@@ -164,9 +169,15 @@ function renderMiniCal() {
`${MONTHS[miniD.getMonth()]} ${miniD.getFullYear()}`;
const firstDay = new Date(miniD.getFullYear(), miniD.getMonth(), 1);
const lastDay = new Date(miniD.getFullYear(), miniD.getMonth() + 1, 0);
const gridStart = new Date(firstDay);
gridStart.setDate(gridStart.getDate() - firstDay.getDay());
gridStart.setDate(gridStart.getDate() - dayOfWeek(firstDay, weekStartDay));
// Update mini-cal DOW headers based on weekStartDay
const miniDowEls = document.querySelectorAll('.mini-cal-grid .mini-dow');
const DOW_MONDAY = ['Mo','Di','Mi','Do','Fr','Sa','So'];
const DOW_SUNDAY = ['So','Mo','Di','Mi','Do','Fr','Sa'];
const DOW_LABELS = weekStartDay === 'sunday' ? DOW_SUNDAY : DOW_MONDAY;
miniDowEls.forEach((el, i) => { el.textContent = DOW_LABELS[i]; });
// Build event date set
const eventDates = new Set(state.events.map(ev => {
@@ -618,10 +629,11 @@ function bindAccountModal() {
// ── Settings Modal ────────────────────────────────────────
function openSettingsModal() {
const s = state.settings;
document.getElementById('cfg-default-view').value = s.default_view || 'month';
document.getElementById('cfg-primary-color').value = s.primary_color || '#4285f4';
document.getElementById('cfg-accent-color').value = s.accent_color || '#ea4335';
document.getElementById('cfg-today-color').value = s.today_color || '#4285f4';
document.getElementById('cfg-default-view').value = s.default_view || 'month';
document.getElementById('cfg-week-start').value = s.week_start_day || 'monday';
document.getElementById('cfg-primary-color').value = s.primary_color || '#4285f4';
document.getElementById('cfg-accent-color').value = s.accent_color || '#ea4335';
document.getElementById('cfg-today-color').value = s.today_color || '#4285f4';
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
document.getElementById('cfg-primary-label').textContent = s.primary_color || '#4285f4';
@@ -701,20 +713,23 @@ function bindSettingsModal() {
document.getElementById('settings-save').onclick = async () => {
const settings = {
default_view: document.getElementById('cfg-default-view').value,
primary_color: document.getElementById('cfg-primary-color').value,
accent_color: document.getElementById('cfg-accent-color').value,
today_color: document.getElementById('cfg-today-color').value,
default_view: document.getElementById('cfg-default-view').value,
week_start_day: document.getElementById('cfg-week-start').value,
primary_color: document.getElementById('cfg-primary-color').value,
accent_color: document.getElementById('cfg-accent-color').value,
today_color: document.getElementById('cfg-today-color').value,
dim_past_events: document.getElementById('cfg-dim-past').checked,
};
try {
await api.put('/settings/', settings);
state.settings = { ...state.settings, ...settings };
state.dimPast = settings.dim_past_events;
weekStartDay = settings.week_start_day;
applyTheme(settings);
showToast('Einstellungen gespeichert');
closeModal('modal-settings');
renderView();
renderMiniCal();
fetchAndRender();
} catch (e) { showToast(e.message, true); }
};
}
@@ -885,12 +900,27 @@ function updateTopbarAvatar(hasAvatar) {
const avatar = document.getElementById('user-avatar');
if (hasAvatar) {
const img = new Image();
img.onload = () => { avatar.textContent = ''; img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%'; avatar.appendChild(img); };
img.onerror = () => { const u = JSON.parse(localStorage.getItem('user')||'{}'); avatar.textContent = (u.username||'?')[0].toUpperCase(); };
img.onload = () => {
avatar.innerHTML = '';
img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
avatar.appendChild(img);
};
img.onerror = () => {
avatar.innerHTML = '';
const u = JSON.parse(localStorage.getItem('user')||'{}');
avatar.textContent = (u.username||'?')[0].toUpperCase();
};
img.src = `/api/profile/avatar?t=${Date.now()}`;
// Update localStorage so avatar persists across reloads
const u = JSON.parse(localStorage.getItem('user')||'{}');
u.has_avatar = true;
localStorage.setItem('user', JSON.stringify(u));
} else {
avatar.innerHTML = '';
const user = JSON.parse(localStorage.getItem('user') || '{}');
avatar.textContent = (user.username || '?')[0].toUpperCase();
user.has_avatar = false;
localStorage.setItem('user', JSON.stringify(user));
}
}
@@ -969,25 +999,34 @@ function openCropModal(file) {
const reader = new FileReader();
reader.onload = (e) => {
const cropImg = document.getElementById('crop-image');
cropImg.src = e.target.result;
openModal('modal-crop');
// Destroy previous cropper if any
if (activeCropper) { activeCropper.destroy(); activeCropper = null; }
// Wait for image to load then init cropper
cropImg.onload = () => {
activeCropper = new Cropper(cropImg, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
autoCropArea: 1,
cropBoxResizable: true,
cropBoxMovable: true,
background: false,
});
};
// Reset image src first to force reload
cropImg.removeAttribute('src');
openModal('modal-crop');
// Use requestAnimationFrame to ensure modal is visible before initializing cropper
requestAnimationFrame(() => {
cropImg.onload = () => {
// Small delay to ensure the image is fully rendered in the DOM
setTimeout(() => {
if (activeCropper) { activeCropper.destroy(); activeCropper = null; }
activeCropper = new Cropper(cropImg, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
autoCropArea: 1,
cropBoxResizable: true,
cropBoxMovable: true,
background: false,
});
}, 100);
};
cropImg.src = e.target.result;
});
};
reader.readAsDataURL(file);
}

248
frontend/js/color-picker.js Normal file
View File

@@ -0,0 +1,248 @@
/* ── Gradient Color Picker (Dark Mode) ─────────────────────
Usage: const hex = await openColorPicker(anchorEl, '#4285f4');
Returns hex string or null if cancelled.
────────────────────────────────────────────────────────── */
// ── HSV ↔ RGB helpers ─────────────────────────────────────
function hsvToRgb(h, s, v) {
h = h / 360 * 6;
const i = Math.floor(h), f = h - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
let r, g, b;
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function rgbToHsv(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
let h = 0, s = max === 0 ? 0 : d / max, v = max;
if (d !== 0) {
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
case g: h = ((b - r) / d + 2); break;
case b: h = ((r - g) / d + 4); break;
}
h *= 60;
}
return [h, s, v];
}
function hexToRgb(hex) {
hex = hex.replace('#', '');
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
return [parseInt(hex.slice(0,2),16), parseInt(hex.slice(2,4),16), parseInt(hex.slice(4,6),16)];
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
}
// ── Active picker tracking ────────────────────────────────
let activePicker = null;
let activeOutsideHandler = null;
function closeActivePicker() {
if (activePicker) {
activePicker.remove();
activePicker = null;
}
if (activeOutsideHandler) {
document.removeEventListener('mousedown', activeOutsideHandler);
activeOutsideHandler = null;
}
}
// ── Main export ───────────────────────────────────────────
export function openColorPicker(anchorEl, currentColor = '#4285f4') {
closeActivePicker();
return new Promise((resolve) => {
let [h, s, v] = rgbToHsv(...hexToRgb(currentColor));
// ── Build DOM ─────────────────────────────────────────
const picker = document.createElement('div');
picker.className = 'gcp';
picker.innerHTML = `
<canvas class="gcp-sv" width="220" height="160"></canvas>
<div class="gcp-sv-cursor"></div>
<div class="gcp-hue-track">
<canvas class="gcp-hue" width="220" height="14"></canvas>
<div class="gcp-hue-thumb"></div>
</div>
<div class="gcp-bottom">
<div class="gcp-preview"></div>
<input class="gcp-hex" type="text" maxlength="7" spellcheck="false" />
</div>
<button class="gcp-select">Auswählen</button>
`;
const svCanvas = picker.querySelector('.gcp-sv');
const svCtx = svCanvas.getContext('2d', { willReadFrequently: true });
const svCursor = picker.querySelector('.gcp-sv-cursor');
const hueCanvas = picker.querySelector('.gcp-hue');
const hueCtx = hueCanvas.getContext('2d');
const hueThumb = picker.querySelector('.gcp-hue-thumb');
const preview = picker.querySelector('.gcp-preview');
const hexInput = picker.querySelector('.gcp-hex');
const selectBtn = picker.querySelector('.gcp-select');
// ── Draw functions ────────────────────────────────────
function drawSV() {
const w = svCanvas.width, hh = svCanvas.height;
// Base hue color
const [r, g, b] = hsvToRgb(h, 1, 1);
// Horizontal: white → hue color (saturation)
const gradH = svCtx.createLinearGradient(0, 0, w, 0);
gradH.addColorStop(0, '#fff');
gradH.addColorStop(1, `rgb(${r},${g},${b})`);
svCtx.fillStyle = gradH;
svCtx.fillRect(0, 0, w, hh);
// Vertical: transparent → black (value)
const gradV = svCtx.createLinearGradient(0, 0, 0, hh);
gradV.addColorStop(0, 'rgba(0,0,0,0)');
gradV.addColorStop(1, '#000');
svCtx.fillStyle = gradV;
svCtx.fillRect(0, 0, w, hh);
}
function drawHue() {
const w = hueCanvas.width, hh = hueCanvas.height;
const grad = hueCtx.createLinearGradient(0, 0, w, 0);
for (let i = 0; i <= 6; i++) {
const [r, g, b] = hsvToRgb(i * 60, 1, 1);
grad.addColorStop(i / 6, `rgb(${r},${g},${b})`);
}
hueCtx.fillStyle = grad;
hueCtx.fillRect(0, 0, w, hh);
}
function updateUI() {
const [r, g, b] = hsvToRgb(h, s, v);
const hex = rgbToHex(r, g, b);
// SV cursor position
svCursor.style.left = (s * svCanvas.width) + 'px';
svCursor.style.top = ((1 - v) * svCanvas.height) + 'px';
// Hue thumb position
hueThumb.style.left = (h / 360 * hueCanvas.width) + 'px';
// Preview + hex
preview.style.background = hex;
hexInput.value = hex.toUpperCase();
}
// ── SV interaction ────────────────────────────────────
function handleSV(e) {
const rect = svCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
s = x / rect.width;
v = 1 - y / rect.height;
updateUI();
}
svCanvas.addEventListener('mousedown', (e) => {
e.preventDefault();
handleSV(e);
const move = (ev) => handleSV(ev);
const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
});
// Touch support for SV
svCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = (ev) => { const t = ev.touches[0]; handleSV(t); };
touch(e);
const end = () => { document.removeEventListener('touchmove', touch); document.removeEventListener('touchend', end); };
document.addEventListener('touchmove', touch);
document.addEventListener('touchend', end);
});
// ── Hue interaction ───────────────────────────────────
function handleHue(e) {
const rect = hueCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
h = (x / rect.width) * 360;
drawSV();
updateUI();
}
const hueTrack = picker.querySelector('.gcp-hue-track');
hueTrack.addEventListener('mousedown', (e) => {
e.preventDefault();
handleHue(e);
const move = (ev) => handleHue(ev);
const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
});
hueTrack.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = (ev) => { const t = ev.touches[0]; handleHue(t); };
touch(e);
const end = () => { document.removeEventListener('touchmove', touch); document.removeEventListener('touchend', end); };
document.addEventListener('touchmove', touch);
document.addEventListener('touchend', end);
});
// ── Hex input ─────────────────────────────────────────
hexInput.addEventListener('change', () => {
let val = hexInput.value.trim();
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
[h, s, v] = rgbToHsv(...hexToRgb(val));
drawSV();
updateUI();
}
});
// ── Select button ─────────────────────────────────────
selectBtn.addEventListener('click', (e) => {
e.stopPropagation();
const [r, g, b] = hsvToRgb(h, s, v);
closeActivePicker();
resolve(rgbToHex(r, g, b));
});
// ── Position picker ───────────────────────────────────
document.body.appendChild(picker);
activePicker = picker;
const anchorRect = anchorEl.getBoundingClientRect();
let top = anchorRect.bottom + 6;
let left = anchorRect.left;
// Keep in viewport
const pRect = picker.getBoundingClientRect();
if (left + pRect.width > window.innerWidth - 8) left = window.innerWidth - pRect.width - 8;
if (left < 8) left = 8;
if (top + pRect.height > window.innerHeight - 8) top = anchorRect.top - pRect.height - 6;
picker.style.top = top + 'px';
picker.style.left = left + 'px';
// ── Initial draw ──────────────────────────────────────
drawSV();
drawHue();
updateUI();
// ── Close on outside click ────────────────────────────
setTimeout(() => {
activeOutsideHandler = (e) => {
if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
closeActivePicker();
resolve(null);
}
};
document.addEventListener('mousedown', activeOutsideHandler);
}, 0);
});
}

View File

@@ -37,6 +37,32 @@ export function toDateInput(d) {
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
// Monday-first: returns 0=Mo, 1=Di, ..., 6=So
export function dayOfWeek(d, weekStartDay = 'monday') {
if (weekStartDay === 'sunday') {
return d.getDay(); // 0=So, 1=Mo, ..., 6=Sa
}
return (d.getDay() + 6) % 7; // 0=Mo, 1=Di, ..., 6=So
}
// Returns the start-of-week date for d
export function weekStart(d, weekStartDay = 'monday') {
const m = new Date(d);
m.setDate(m.getDate() - dayOfWeek(m, weekStartDay));
m.setHours(0, 0, 0, 0);
return m;
}
// Returns the ISO week number (Monday-based, ISO 8601)
export function getISOWeekNumber(d) {
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
// ISO week: weeks start on Monday, week 1 contains the first Thursday
const day = date.getUTCDay() || 7; // make Sunday = 7
date.setUTCDate(date.getUTCDate() + 4 - day);
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
return Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
}
export function applyTheme(settings) {
const root = document.documentElement;
root.style.setProperty('--primary', settings.primary_color || '#4285f4');

View File

@@ -1,17 +1,20 @@
import { formatDate, isSameDay, isToday, isPast } from '../utils.js';
import { formatDate, isSameDay, isToday, isPast, dayOfWeek, getISOWeekNumber } from '../utils.js';
const DOW = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
const DOW_MONDAY = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
const DOW_SUNDAY = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
export function renderMonth(container, currentDate, events, onDayClick, onEventClick) {
export function renderMonth(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const DOW = weekStartDay === 'sunday' ? DOW_SUNDAY : DOW_MONDAY;
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Start grid on Sunday of the week containing the 1st
// Start grid on the correct weekday
const gridStart = new Date(firstDay);
gridStart.setDate(gridStart.getDate() - firstDay.getDay());
const offset = dayOfWeek(firstDay, weekStartDay);
gridStart.setDate(gridStart.getDate() - offset);
const cells = [];
const d = new Date(gridStart);
@@ -39,45 +42,55 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
}
});
// Header
const headerHtml = DOW.map(d => `<div class="month-dow">${d}</div>`).join('');
// Header: KW-Spalte + Wochentage
const headerHtml = `<div class="month-kw-header">KW</div>` +
DOW.map(d => `<div class="month-dow">${d}</div>`).join('');
// Cells
const cellsHtml = cells.map(cell => {
const key = dateKey(cell);
const cellEvs = (evMap[key] || []).slice().sort((a, b) => {
if (a.allDay && !b.allDay) return -1;
if (!a.allDay && b.allDay) return 1;
return new Date(a.start) - new Date(b.start);
});
// Build rows (6 weeks × 7 days)
let cellsHtml = '';
for (let row = 0; row < 6; row++) {
// KW cell for the first day of this row
const rowFirstDay = cells[row * 7];
const kw = getISOWeekNumber(rowFirstDay);
cellsHtml += `<div class="month-kw-cell">${kw}</div>`;
const isOther = cell.getMonth() !== month;
const todayClass = isToday(cell) ? 'today' : '';
const otherClass = isOther ? 'other-month' : '';
const numClass = isToday(cell) ? 'today' : '';
for (let col = 0; col < 7; col++) {
const cell = cells[row * 7 + col];
const key = dateKey(cell);
const cellEvs = (evMap[key] || []).slice().sort((a, b) => {
if (a.allDay && !b.allDay) return -1;
if (!a.allDay && b.allDay) return 1;
return new Date(a.start) - new Date(b.start);
});
const MAX_VISIBLE = 3;
const visible = cellEvs.slice(0, MAX_VISIBLE);
const hiddenCount = cellEvs.length - MAX_VISIBLE;
const isOther = cell.getMonth() !== month;
const todayClass = isToday(cell) ? 'today' : '';
const otherClass = isOther ? 'other-month' : '';
const numClass = isToday(cell) ? 'today' : '';
const evHtml = visible.map(ev => {
const color = ev.color || ev.calendarColor || '#4285f4';
const pastClass = isPast(ev) ? 'past' : '';
const title = ev.allDay ? ev.title : `${fmtTime(new Date(ev.start))} ${ev.title}`;
return `<div class="month-event ${pastClass}" data-id="${ev.id}" data-url="${escAttr(ev.url)}"
style="background:${color};color:#fff"
title="${escAttr(ev.title)}">${escHtml(title)}</div>`;
}).join('');
const MAX_VISIBLE = 3;
const visible = cellEvs.slice(0, MAX_VISIBLE);
const hiddenCount = cellEvs.length - MAX_VISIBLE;
const moreHtml = hiddenCount > 0
? `<div class="month-more" data-date="${key}">+${hiddenCount} weitere</div>`
: '';
const evHtml = visible.map(ev => {
const color = ev.color || ev.calendarColor || '#4285f4';
const pastClass = isPast(ev) ? 'past' : '';
const title = ev.allDay ? ev.title : `${fmtTime(new Date(ev.start))} ${ev.title}`;
return `<div class="month-event ${pastClass}" data-id="${ev.id}" data-url="${escAttr(ev.url)}"
style="background:${color};color:#fff"
title="${escAttr(ev.title)}">${escHtml(title)}</div>`;
}).join('');
return `<div class="month-cell ${todayClass} ${otherClass}" data-date="${key}">
<div class="cell-day ${numClass}">${cell.getDate()}</div>
${evHtml}${moreHtml}
</div>`;
}).join('');
const moreHtml = hiddenCount > 0
? `<div class="month-more" data-date="${key}">+${hiddenCount} weitere</div>`
: '';
cellsHtml += `<div class="month-cell ${todayClass} ${otherClass}" data-date="${key}">
<div class="cell-day ${numClass}">${cell.getDate()}</div>
${evHtml}${moreHtml}
</div>`;
}
}
container.innerHTML = `<div class="month-view">
<div class="month-header">${headerHtml}</div>

View File

@@ -1,17 +1,16 @@
import { isToday, isPast } from '../utils.js';
import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../utils.js';
const DOW_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false) {
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday') {
// Build the days array (7 days for week, 1 for day)
const days = [];
if (isSingleDay) {
days.push(new Date(currentDate));
} else {
const sunday = new Date(currentDate);
sunday.setDate(sunday.getDate() - sunday.getDay());
const monday = weekStart(currentDate, weekStartDay);
for (let i = 0; i < 7; i++) {
const d = new Date(sunday);
const d = new Date(monday);
d.setDate(d.getDate() + i);
days.push(d);
}
@@ -21,6 +20,12 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
const allDayEvs = events.filter(ev => ev.allDay);
const timedEvs = events.filter(ev => !ev.allDay);
// ── KW Badge ──────────────────────────────────────────
const kwNum = getISOWeekNumber(days[0]);
const kwBadge = !isSingleDay
? `<div class="week-kw-badge">KW ${kwNum}</div>`
: '';
// ── Header ────────────────────────────────────────────
const headerCols = days.map(day => {
const todayCls = isToday(day) ? 'today' : '';
@@ -98,7 +103,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
container.innerHTML = `<div class="${viewClass}">
<div class="week-header-row">
<div class="week-time-gutter"></div>
<div class="week-time-gutter">${kwBadge}</div>
${headerCols}
</div>
<div class="week-allday-row">