feat: show location in events, fix HTML in notes, fix CalDAV TZID, reduce collapsed all-day space

- Strip HTML tags from descriptions before sending to CalDAV providers (Apple/Synology)
- Show event location underneath time in calendar event blocks (like Google Calendar)
- Fix German umlaut rendering by adding latin-ext font subset
- Fix CalDAV event updates losing TZID on DTSTART/DTEND (Apple Calendar recurring edit fix)
- Add error logging for CalDAV PUT responses
- Reduce wasted space when all-day events section is collapsed (24px max)
- Fix quotes with missing umlauts (ä, ö, ü, ß)

v1.71.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-24 22:14:21 +01:00
parent 948d239867
commit da2ebbedb5
8 changed files with 90 additions and 50 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.70.0", "version": "1.71.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -3177,6 +3177,14 @@ h3 {
flex-shrink: 0; flex-shrink: 0;
} }
.all-day-events-section.collapsed {
height: 24px !important;
min-height: 24px;
max-height: 24px;
overflow: hidden;
padding: 0;
}
/* Resize handle for draggable section borders */ /* Resize handle for draggable section borders */
.resize-handle { .resize-handle {
height: 7px; height: 7px;
@ -4120,6 +4128,15 @@ h3 {
opacity: 0.9; opacity: 0.9;
} }
.event-location-row {
font-family: var(--weekly-event-font);
font-size: 0.65rem;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Header Icons */ /* Header Icons */
.weekly-btn-icon { .weekly-btn-icon {
display: flex; display: flex;

View File

@ -3,7 +3,7 @@ import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import { Providers } from './providers'; import { Providers } from './providers';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin', 'latin-ext'] });
export const viewport: Viewport = { export const viewport: Viewport = {
width: 'device-width', width: 'device-width',

View File

@ -245,7 +245,7 @@ const useGoogleFonts = (fonts: string[]) => {
let link = document.getElementById(linkId) as HTMLLinkElement; let link = document.getElementById(linkId) as HTMLLinkElement;
const fontQuery = fontsToLoad.map((f) => f.replace(" ", "+")).join("|"); const fontQuery = fontsToLoad.map((f) => f.replace(" ", "+")).join("|");
const href = `https://fonts.googleapis.com/css2?family=${fontsToLoad.map((f) => `${f.replace(" ", "+")}:wght@300;400;500;700`).join("&family=")}&display=swap`; const href = `https://fonts.googleapis.com/css2?family=${fontsToLoad.map((f) => `${f.replace(" ", "+")}:wght@300;400;500;700`).join("&family=")}&subset=latin,latin-ext&display=swap`;
if (!link) { if (!link) {
link = document.createElement("link"); link = document.createElement("link");
@ -2780,7 +2780,9 @@ export default function WeeklyView() {
if (!res.ok) continue; if (!res.ok) continue;
const contentType = res.headers.get("content-type") || ""; const contentType = res.headers.get("content-type") || "";
if (!contentType.includes("application/json")) continue; if (!contentType.includes("application/json")) continue;
const data = await res.json(); // Ensure proper UTF-8 decoding for quotes with special characters
const rawText = await res.text();
const data = JSON.parse(rawText);
let quoteText = ""; let quoteText = "";
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
@ -7861,6 +7863,9 @@ export default function WeeklyView() {
</span> </span>
</div> </div>
<div className="event-time-row">{timeStr}</div> <div className="event-time-row">{timeStr}</div>
{event.location && (
<div className="event-location-row">{event.location}</div>
)}
{(event.isRecurring || event.description || profile.showCalendarProviderIcon) && ( {(event.isRecurring || event.description || profile.showCalendarProviderIcon) && (
<div className="event-icons-row"> <div className="event-icons-row">
{event.description && ( {event.description && (
@ -8002,6 +8007,9 @@ export default function WeeklyView() {
> >
{event.title} {event.title}
</div> </div>
{event.location && (
<div className="event-location-row" style={{ color: "inherit", opacity: 0.7 }}>{event.location}</div>
)}
{(event.isRecurring || event.description || profile.showCalendarProviderIcon) && ( {(event.isRecurring || event.description || profile.showCalendarProviderIcon) && (
<div className="event-icons-row"> <div className="event-icons-row">
{event.description && ( {event.description && (

View File

@ -701,25 +701,32 @@ export const updateEvent = async (
const existingDtStart = vevent.getFirstProperty('dtstart'); const existingDtStart = vevent.getFirstProperty('dtstart');
const existingTzid = existingDtStart?.getParameter('tzid') as string | undefined; const existingTzid = existingDtStart?.getParameter('tzid') as string | undefined;
// Helper to convert UTC dateTime to local ICAL.Time preserving TZID
const toLocalIcalTime = (dateTimeStr: string, tzid: string) => {
const d = new Date(dateTimeStr);
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: tzid, year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).formatToParts(d);
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
const tz = ICAL.Timezone.fromData({ tzid, component: comp.getFirstSubcomponent('vtimezone') || undefined });
return ICAL.Time.fromData({
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
isDate: false,
}, tz);
};
if (eventData.start) { if (eventData.start) {
if (eventData.start.date) { if (eventData.start.date) {
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true); event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
event.startDate.isDate = true; event.startDate.isDate = true;
} else if (eventData.start.dateTime) { } else if (eventData.start.dateTime) {
if (existingTzid) { if (existingTzid) {
// Convert UTC dateTime to local time in the TZID event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
const d = new Date(eventData.start.dateTime); // Ensure TZID parameter is preserved on the property
const parts = new Intl.DateTimeFormat('en-CA', { const dtStartProp = vevent.getFirstProperty('dtstart');
timeZone: existingTzid, year: 'numeric', month: '2-digit', day: '2-digit', if (dtStartProp) dtStartProp.setParameter('tzid', existingTzid);
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).formatToParts(d);
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
const localTime = ICAL.Time.fromData({
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
isDate: false,
});
event.startDate = localTime;
} else { } else {
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true); event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
event.startDate.isDate = false; event.startDate.isDate = false;
@ -733,18 +740,9 @@ export const updateEvent = async (
event.endDate.isDate = true; event.endDate.isDate = true;
} else if (eventData.end.dateTime) { } else if (eventData.end.dateTime) {
if (existingTzid) { if (existingTzid) {
const d = new Date(eventData.end.dateTime); event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
const parts = new Intl.DateTimeFormat('en-CA', { const dtEndProp = vevent.getFirstProperty('dtend');
timeZone: existingTzid, year: 'numeric', month: '2-digit', day: '2-digit', if (dtEndProp) dtEndProp.setParameter('tzid', existingTzid);
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).formatToParts(d);
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
const localTime = ICAL.Time.fromData({
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
isDate: false,
});
event.endDate = localTime;
} else { } else {
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true); event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
event.endDate.isDate = false; event.endDate.isDate = false;
@ -838,12 +836,19 @@ export const updateEvent = async (
// Let's try using `client.updateObject` directly which is more raw but allows data. // Let's try using `client.updateObject` directly which is more raw but allows data.
// `targetObject.url` is what we need. // `targetObject.url` is what we need.
await client.updateObject({ const updateResult = await client.updateObject({
url: targetObject.url, url: targetObject.url,
data: updatedIcalString, data: updatedIcalString,
etag: targetObject.etag etag: targetObject.etag
} as any); // Cast to any to bypass type definition mismatch } as any); // Cast to any to bypass type definition mismatch
// Log result for debugging
if (updateResult && (updateResult as any).status && (updateResult as any).status >= 400) {
console.error('[APPLE CALENDAR] Update failed with status:', (updateResult as any).status);
throw new Error(`CalDAV update failed with status ${(updateResult as any).status}`);
}
console.log('[APPLE CALENDAR] Event updated successfully');
return { return {
id: eventId, id: eventId,
title: event.summary, title: event.summary,

View File

@ -956,7 +956,7 @@ export const createCalendarEvent = async (
const createdEvent = await import('./apple-calendar').then(m => const createdEvent = await import('./apple-calendar').then(m =>
m.createEvent(email, appPassword, calendarId, { m.createEvent(email, appPassword, calendarId, {
title: event.title!, title: event.title!,
description: event.description, description: stripHtmlForCalDav(event.description),
location: event.location, location: event.location,
url: event.url, url: event.url,
recurrence: event.recurrence, recurrence: event.recurrence,
@ -997,7 +997,7 @@ export const createCalendarEvent = async (
const createdEvent = await import('./synology-calendar').then(m => const createdEvent = await import('./synology-calendar').then(m =>
m.createEvent(serverUrl, username, password, calendarId, { m.createEvent(serverUrl, username, password, calendarId, {
title: event.title!, title: event.title!,
description: event.description, description: stripHtmlForCalDav(event.description),
location: event.location, location: event.location,
url: event.url, url: event.url,
recurrence: event.recurrence, recurrence: event.recurrence,
@ -1066,6 +1066,11 @@ export const createCalendarEvent = async (
/** /**
* Update an existing calendar event * Update an existing calendar event
*/ */
const stripHtmlForCalDav = (html: string | undefined): string | undefined => {
if (!html) return html;
return html.replace(/<[^>]*>/g, '').trim();
};
export const updateCalendarEvent = async ( export const updateCalendarEvent = async (
connection: CalendarConnection, connection: CalendarConnection,
calendarId: string, calendarId: string,
@ -1186,7 +1191,7 @@ export const updateCalendarEvent = async (
const updatedEvent = await import('./apple-calendar').then(m => const updatedEvent = await import('./apple-calendar').then(m =>
m.updateEvent(email, appPassword, calendarId, eventId, { m.updateEvent(email, appPassword, calendarId, eventId, {
title: event.title, title: event.title,
description: event.description, description: stripHtmlForCalDav(event.description),
location: event.location, location: event.location,
url: event.url, url: event.url,
start: event.start, start: event.start,
@ -1220,7 +1225,7 @@ export const updateCalendarEvent = async (
const updatedEvent = await import('./synology-calendar').then(m => const updatedEvent = await import('./synology-calendar').then(m =>
m.updateEvent(serverUrl, username, password, calendarId, eventId, { m.updateEvent(serverUrl, username, password, calendarId, eventId, {
title: event.title, title: event.title,
description: event.description, description: stripHtmlForCalDav(event.description),
location: event.location, location: event.location,
url: event.url, url: event.url,
start: event.start, start: event.start,

View File

@ -55,7 +55,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["life", "wisdom"], tags: ["life", "wisdom"],
}, },
{ {
text: "Man muss das Unmogliche versuchen, um das Mogliche zu erreichen.", text: "Man muss das Unmögliche versuchen, um das Mögliche zu erreichen.",
author: "Hermann Hesse", author: "Hermann Hesse",
language: "de", language: "de",
tags: ["motivation", "perseverance"], tags: ["motivation", "perseverance"],
@ -67,13 +67,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["wisdom", "perseverance", "life"], tags: ["wisdom", "perseverance", "life"],
}, },
{ {
text: "Es hort doch jeder nur, was er versteht.", text: "Es hört doch jeder nur, was er versteht.",
author: "Johann Wolfgang von Goethe", author: "Johann Wolfgang von Goethe",
language: "de", language: "de",
tags: ["wisdom", "life"], tags: ["wisdom", "life"],
}, },
{ {
text: "Ohne Musik ware das Leben ein Irrtum.", text: "Ohne Musik wäre das Leben ein Irrtum.",
author: "Friedrich Nietzsche", author: "Friedrich Nietzsche",
language: "de", language: "de",
tags: ["life", "creativity"], tags: ["life", "creativity"],
@ -91,7 +91,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["wisdom", "life", "motivation"], tags: ["wisdom", "life", "motivation"],
}, },
{ {
text: "Wer kampft, kann verlieren. Wer nicht kampft, hat schon verloren.", text: "Wer kämpft, kann verlieren. Wer nicht kämpft, hat schon verloren.",
author: "Bertolt Brecht", author: "Bertolt Brecht",
language: "de", language: "de",
tags: ["motivation", "perseverance", "success"], tags: ["motivation", "perseverance", "success"],
@ -103,7 +103,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["wisdom", "creativity"], tags: ["wisdom", "creativity"],
}, },
{ {
text: "Was mich nicht umbringt, macht mich starker.", text: "Was mich nicht umbringt, macht mich stärker.",
author: "Friedrich Nietzsche", author: "Friedrich Nietzsche",
language: "de", language: "de",
tags: ["perseverance", "motivation"], tags: ["perseverance", "motivation"],
@ -127,7 +127,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["wisdom", "motivation", "work"], tags: ["wisdom", "motivation", "work"],
}, },
{ {
text: "Auch aus Steinen, die einem in den Weg gelegt werden, kann man Schones bauen.", text: "Auch aus Steinen, die einem in den Weg gelegt werden, kann man Schönes bauen.",
author: "Johann Wolfgang von Goethe", author: "Johann Wolfgang von Goethe",
language: "de", language: "de",
tags: ["perseverance", "creativity", "life"], tags: ["perseverance", "creativity", "life"],
@ -151,13 +151,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["creativity", "life", "humor"], tags: ["creativity", "life", "humor"],
}, },
{ {
text: "Lache nie uber die Dummheit der anderen. Sie ist deine Chance.", text: "Lache nie über die Dummheit der anderen. Sie ist deine Chance.",
author: "Winston Churchill", author: "Winston Churchill",
language: "de", language: "de",
tags: ["humor", "success", "wisdom"], tags: ["humor", "success", "wisdom"],
}, },
{ {
text: "Leben ist das, was passiert, wahrend du eifrig dabei bist, andere Plane zu machen.", text: "Leben ist das, was passiert, während du eifrig dabei bist, andere Pläne zu machen.",
author: "John Lennon", author: "John Lennon",
language: "de", language: "de",
tags: ["life", "humor", "wisdom"], tags: ["life", "humor", "wisdom"],
@ -345,7 +345,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
}, },
// --- Additional German Quotes --- // --- Additional German Quotes ---
{ {
text: "Das Geheimnis des Vorwartskommens besteht darin, den ersten Schritt zu tun.", text: "Das Geheimnis des Vorwärtskommens besteht darin, den ersten Schritt zu tun.",
author: "Mark Twain", author: "Mark Twain",
language: "de", language: "de",
tags: ["motivation", "work"], tags: ["motivation", "work"],
@ -363,13 +363,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["motivation", "work", "success"], tags: ["motivation", "work", "success"],
}, },
{ {
text: "Die Zukunft gehort denen, die an die Schonheit ihrer Traume glauben.", text: "Die Zukunft gehört denen, die an die Schönheit ihrer Träume glauben.",
author: "Eleanor Roosevelt", author: "Eleanor Roosevelt",
language: "de", language: "de",
tags: ["motivation", "life", "creativity"], tags: ["motivation", "life", "creativity"],
}, },
{ {
text: "Sei du selbst die Veranderung, die du dir wunschst fur diese Welt.", text: "Sei du selbst die Veränderung, die du dir wünschst für diese Welt.",
author: "Mahatma Gandhi", author: "Mahatma Gandhi",
language: "de", language: "de",
tags: ["wisdom", "life", "motivation"], tags: ["wisdom", "life", "motivation"],
@ -381,7 +381,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["motivation", "life"], tags: ["motivation", "life"],
}, },
{ {
text: "Wer aufhort besser zu werden, hat aufgehort gut zu sein.", text: "Wer aufhört besser zu werden, hat aufgehört gut zu sein.",
author: "Philip Rosenthal", author: "Philip Rosenthal",
language: "de", language: "de",
tags: ["perseverance", "work", "success"], tags: ["perseverance", "work", "success"],
@ -393,13 +393,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["humor", "creativity", "wisdom"], tags: ["humor", "creativity", "wisdom"],
}, },
{ {
text: "In der Mitte von Schwierigkeiten liegen die Moglichkeiten.", text: "In der Mitte von Schwierigkeiten liegen die Möglichkeiten.",
author: "Albert Einstein", author: "Albert Einstein",
language: "de", language: "de",
tags: ["perseverance", "motivation"], tags: ["perseverance", "motivation"],
}, },
{ {
text: "Es gibt keine Abkurzung zu einem Ort, der es wert ist, erreicht zu werden.", text: "Es gibt keine Abkürzung zu einem Ort, der es wert ist, erreicht zu werden.",
author: "Beverly Sills", author: "Beverly Sills",
language: "de", language: "de",
tags: ["perseverance", "success", "work"], tags: ["perseverance", "success", "work"],
@ -411,7 +411,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
tags: ["wisdom", "life"], tags: ["wisdom", "life"],
}, },
{ {
text: "Kreativitat ist Intelligenz, die Spass hat.", text: "Kreativität ist Intelligenz, die Spaß hat.",
author: "Albert Einstein", author: "Albert Einstein",
language: "de", language: "de",
tags: ["creativity", "humor"], tags: ["creativity", "humor"],

View File

@ -725,11 +725,12 @@ export const updateEvent = async (
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).formatToParts(d); }).formatToParts(d);
const get = (t: string) => parts.find(p => p.type === t)?.value || '00'; const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
const tz = ICAL.Timezone.fromData({ tzid, component: comp.getFirstSubcomponent('vtimezone') || undefined });
return ICAL.Time.fromData({ return ICAL.Time.fromData({
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')), year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')), hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
isDate: false, isDate: false,
}); }, tz);
}; };
if (eventData.start) { if (eventData.start) {
@ -739,6 +740,8 @@ export const updateEvent = async (
} else if (eventData.start.dateTime) { } else if (eventData.start.dateTime) {
if (existingTzid) { if (existingTzid) {
event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid); event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
const dtStartProp = vevent.getFirstProperty('dtstart');
if (dtStartProp) dtStartProp.setParameter('tzid', existingTzid);
} else { } else {
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true); event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
event.startDate.isDate = false; event.startDate.isDate = false;
@ -753,6 +756,8 @@ export const updateEvent = async (
} else if (eventData.end.dateTime) { } else if (eventData.end.dateTime) {
if (existingTzid) { if (existingTzid) {
event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid); event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
const dtEndProp = vevent.getFirstProperty('dtend');
if (dtEndProp) dtEndProp.setParameter('tzid', existingTzid);
} else { } else {
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true); event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
event.endDate.isDate = false; event.endDate.isDate = false;