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:
parent
948d239867
commit
da2ebbedb5
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -3177,6 +3177,14 @@ h3 {
|
||||
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 {
|
||||
height: 7px;
|
||||
@ -4120,6 +4128,15 @@ h3 {
|
||||
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 */
|
||||
.weekly-btn-icon {
|
||||
display: flex;
|
||||
|
||||
@ -3,7 +3,7 @@ import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Providers } from './providers';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
const inter = Inter({ subsets: ['latin', 'latin-ext'] });
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
|
||||
@ -245,7 +245,7 @@ const useGoogleFonts = (fonts: string[]) => {
|
||||
let link = document.getElementById(linkId) as HTMLLinkElement;
|
||||
|
||||
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) {
|
||||
link = document.createElement("link");
|
||||
@ -2780,7 +2780,9 @@ export default function WeeklyView() {
|
||||
if (!res.ok) continue;
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
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 = "";
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
@ -7861,6 +7863,9 @@ export default function WeeklyView() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="event-time-row">{timeStr}</div>
|
||||
{event.location && (
|
||||
<div className="event-location-row">{event.location}</div>
|
||||
)}
|
||||
{(event.isRecurring || event.description || profile.showCalendarProviderIcon) && (
|
||||
<div className="event-icons-row">
|
||||
{event.description && (
|
||||
@ -8002,6 +8007,9 @@ export default function WeeklyView() {
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
{event.location && (
|
||||
<div className="event-location-row" style={{ color: "inherit", opacity: 0.7 }}>{event.location}</div>
|
||||
)}
|
||||
{(event.isRecurring || event.description || profile.showCalendarProviderIcon) && (
|
||||
<div className="event-icons-row">
|
||||
{event.description && (
|
||||
|
||||
@ -701,25 +701,32 @@ export const updateEvent = async (
|
||||
const existingDtStart = vevent.getFirstProperty('dtstart');
|
||||
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.date) {
|
||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||
event.startDate.isDate = true;
|
||||
} else if (eventData.start.dateTime) {
|
||||
if (existingTzid) {
|
||||
// Convert UTC dateTime to local time in the TZID
|
||||
const d = new Date(eventData.start.dateTime);
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: existingTzid, 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 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;
|
||||
event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
|
||||
// Ensure TZID parameter is preserved on the property
|
||||
const dtStartProp = vevent.getFirstProperty('dtstart');
|
||||
if (dtStartProp) dtStartProp.setParameter('tzid', existingTzid);
|
||||
} else {
|
||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||
event.startDate.isDate = false;
|
||||
@ -733,18 +740,9 @@ export const updateEvent = async (
|
||||
event.endDate.isDate = true;
|
||||
} else if (eventData.end.dateTime) {
|
||||
if (existingTzid) {
|
||||
const d = new Date(eventData.end.dateTime);
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: existingTzid, 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 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;
|
||||
event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
|
||||
const dtEndProp = vevent.getFirstProperty('dtend');
|
||||
if (dtEndProp) dtEndProp.setParameter('tzid', existingTzid);
|
||||
} else {
|
||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||
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.
|
||||
// `targetObject.url` is what we need.
|
||||
|
||||
await client.updateObject({
|
||||
const updateResult = await client.updateObject({
|
||||
url: targetObject.url,
|
||||
data: updatedIcalString,
|
||||
etag: targetObject.etag
|
||||
} 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 {
|
||||
id: eventId,
|
||||
title: event.summary,
|
||||
|
||||
@ -956,7 +956,7 @@ export const createCalendarEvent = async (
|
||||
const createdEvent = await import('./apple-calendar').then(m =>
|
||||
m.createEvent(email, appPassword, calendarId, {
|
||||
title: event.title!,
|
||||
description: event.description,
|
||||
description: stripHtmlForCalDav(event.description),
|
||||
location: event.location,
|
||||
url: event.url,
|
||||
recurrence: event.recurrence,
|
||||
@ -997,7 +997,7 @@ export const createCalendarEvent = async (
|
||||
const createdEvent = await import('./synology-calendar').then(m =>
|
||||
m.createEvent(serverUrl, username, password, calendarId, {
|
||||
title: event.title!,
|
||||
description: event.description,
|
||||
description: stripHtmlForCalDav(event.description),
|
||||
location: event.location,
|
||||
url: event.url,
|
||||
recurrence: event.recurrence,
|
||||
@ -1066,6 +1066,11 @@ export const createCalendarEvent = async (
|
||||
/**
|
||||
* 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 (
|
||||
connection: CalendarConnection,
|
||||
calendarId: string,
|
||||
@ -1186,7 +1191,7 @@ export const updateCalendarEvent = async (
|
||||
const updatedEvent = await import('./apple-calendar').then(m =>
|
||||
m.updateEvent(email, appPassword, calendarId, eventId, {
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
description: stripHtmlForCalDav(event.description),
|
||||
location: event.location,
|
||||
url: event.url,
|
||||
start: event.start,
|
||||
@ -1220,7 +1225,7 @@ export const updateCalendarEvent = async (
|
||||
const updatedEvent = await import('./synology-calendar').then(m =>
|
||||
m.updateEvent(serverUrl, username, password, calendarId, eventId, {
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
description: stripHtmlForCalDav(event.description),
|
||||
location: event.location,
|
||||
url: event.url,
|
||||
start: event.start,
|
||||
|
||||
@ -55,7 +55,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["motivation", "perseverance"],
|
||||
@ -67,13 +67,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["wisdom", "life"],
|
||||
},
|
||||
{
|
||||
text: "Ohne Musik ware das Leben ein Irrtum.",
|
||||
text: "Ohne Musik wäre das Leben ein Irrtum.",
|
||||
author: "Friedrich Nietzsche",
|
||||
language: "de",
|
||||
tags: ["life", "creativity"],
|
||||
@ -91,7 +91,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["motivation", "perseverance", "success"],
|
||||
@ -103,7 +103,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
tags: ["wisdom", "creativity"],
|
||||
},
|
||||
{
|
||||
text: "Was mich nicht umbringt, macht mich starker.",
|
||||
text: "Was mich nicht umbringt, macht mich stärker.",
|
||||
author: "Friedrich Nietzsche",
|
||||
language: "de",
|
||||
tags: ["perseverance", "motivation"],
|
||||
@ -127,7 +127,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["perseverance", "creativity", "life"],
|
||||
@ -151,13 +151,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["life", "humor", "wisdom"],
|
||||
@ -345,7 +345,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
},
|
||||
// --- 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",
|
||||
language: "de",
|
||||
tags: ["motivation", "work"],
|
||||
@ -363,13 +363,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["wisdom", "life", "motivation"],
|
||||
@ -381,7 +381,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["perseverance", "work", "success"],
|
||||
@ -393,13 +393,13 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
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",
|
||||
language: "de",
|
||||
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",
|
||||
language: "de",
|
||||
tags: ["perseverance", "success", "work"],
|
||||
@ -411,7 +411,7 @@ export const LOCAL_QUOTES: LocalQuote[] = [
|
||||
tags: ["wisdom", "life"],
|
||||
},
|
||||
{
|
||||
text: "Kreativitat ist Intelligenz, die Spass hat.",
|
||||
text: "Kreativität ist Intelligenz, die Spaß hat.",
|
||||
author: "Albert Einstein",
|
||||
language: "de",
|
||||
tags: ["creativity", "humor"],
|
||||
|
||||
@ -725,11 +725,12 @@ export const updateEvent = async (
|
||||
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) {
|
||||
@ -739,6 +740,8 @@ export const updateEvent = async (
|
||||
} else if (eventData.start.dateTime) {
|
||||
if (existingTzid) {
|
||||
event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
|
||||
const dtStartProp = vevent.getFirstProperty('dtstart');
|
||||
if (dtStartProp) dtStartProp.setParameter('tzid', existingTzid);
|
||||
} else {
|
||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||
event.startDate.isDate = false;
|
||||
@ -753,6 +756,8 @@ export const updateEvent = async (
|
||||
} else if (eventData.end.dateTime) {
|
||||
if (existingTzid) {
|
||||
event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
|
||||
const dtEndProp = vevent.getFirstProperty('dtend');
|
||||
if (dtEndProp) dtEndProp.setParameter('tzid', existingTzid);
|
||||
} else {
|
||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||
event.endDate.isDate = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user