Skip to content

Commit 801ce90

Browse files
authored
agenda: add time format setting and fix timezone issues (#140)
- Add 12h/24h time format preference (default: 12h) - Fix events appearing on wrong day by using UTC for date grouping - Fix time display offset by using timeZone: UTC (node-ical stores local as UTC) - Fix section title date parsing to avoid timezone shifts - Extract usePreferences hook for centralized preference access - Add Preferences type to types.ts Fixes #138
1 parent b214f40 commit 801ce90

File tree

9 files changed

+83
-34
lines changed

9 files changed

+83
-34
lines changed

extensions/agenda/package-lock.json

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/agenda/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@
5757
}
5858
],
5959
"required": false
60+
},
61+
{
62+
"name": "timeFormat",
63+
"type": "dropdown",
64+
"title": "Time Format",
65+
"description": "How to display event times",
66+
"default": "12h",
67+
"data": [
68+
{
69+
"title": "12-hour (2:30 PM)",
70+
"value": "12h"
71+
},
72+
{
73+
"title": "24-hour (14:30)",
74+
"value": "24h"
75+
}
76+
],
77+
"required": false
6078
}
6179
],
6280
"scripts": {

extensions/agenda/src/components/EventListItem.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Action, ActionPanel, Icon, List } from "@vicinae/api";
22
import type { VEvent } from "node-ical";
33
import { Calendar } from "../types";
4+
import { usePreferences } from "../hooks/usePreferences";
45
import { getCalendarName } from "../utils/calendar";
56
import { getSupportedUrls, urlHandlers } from "../utils/urls";
67
import { isAllDayEvent, getDisplayStart, getDisplayEnd } from "../utils/events";
@@ -20,11 +21,12 @@ export function EventListItem({
2021
onToggleDetail,
2122
calendars,
2223
}: EventListItemProps) {
24+
const { use24Hour } = usePreferences();
2325
const startDate = new Date(event.start);
2426
const endDate = new Date(event.end);
2527
const isAllDay = isAllDayEvent(startDate, endDate);
26-
const displayStart = getDisplayStart(startDate, isAllDay);
27-
const displayEnd = getDisplayEnd(endDate, isAllDay);
28+
const displayStart = getDisplayStart(startDate, isAllDay, use24Hour);
29+
const displayEnd = getDisplayEnd(endDate, isAllDay, use24Hour);
2830
const calendar = eventCalendarUrl
2931
? calendars.find((cal) => cal.url === eventCalendarUrl)
3032
: undefined;

extensions/agenda/src/hooks/useCalendarData.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { VEvent } from "node-ical";
55
import { Calendar } from "../types";
66
import { getCalendars, getCalendarName } from "../utils/calendar";
77
import { saveToCache, loadFromCache } from "../utils/cache";
8-
import { isAllDayEvent, getDisplayStart } from "../utils/events";
8+
import { isAllDayEvent, getDisplayStart, getLocalDateString } from "../utils/events";
99
import { CACHE_KEY } from "../constants";
1010

1111
export function useCalendarData(refreshInterval: number) {
@@ -85,7 +85,7 @@ export function useCalendarData(refreshInterval: number) {
8585
...item,
8686
start: occurrenceStartDate,
8787
end: occurrenceEndDate,
88-
uid: `${item.uid}_${occurrenceStartDate.toISOString().split("T")[0]}`, // Make UID unique for each occurrence
88+
uid: `${item.uid}_${getLocalDateString(occurrenceStartDate)}`, // Make UID unique for each occurrence
8989
recurrenceId: occurrenceStartDate,
9090
};
9191

@@ -141,21 +141,13 @@ export function useCalendarData(refreshInterval: number) {
141141
return a.summary.localeCompare(b.summary);
142142
}
143143

144-
// Both timed events
145-
const aDisplayStart = getDisplayStart(aStart, aIsAllDay);
146-
const bDisplayStart = getDisplayStart(bStart, bIsAllDay);
147-
return (
148-
new Date(
149-
aStart.toISOString().split("T")[0] + "T" + aDisplayStart
150-
).getTime() -
151-
new Date(
152-
bStart.toISOString().split("T")[0] + "T" + bDisplayStart
153-
).getTime()
154-
);
144+
// Both timed events - sort by actual start time
145+
return aStart.getTime() - bStart.getTime();
155146
});
156147

157148
for (const event of allEvents) {
158-
const eventDate = new Date(event.start).toISOString().split("T")[0];
149+
// Use local date to prevent timezone-related day shift issues
150+
const eventDate = getLocalDateString(new Date(event.start));
159151
if (!eventsByDate[eventDate]) {
160152
eventsByDate[eventDate] = [];
161153
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getPreferenceValues } from "@vicinae/api";
2+
import { Preferences } from "../types";
3+
4+
export function usePreferences() {
5+
const preferences = getPreferenceValues<Preferences>();
6+
return {
7+
refreshInterval: parseInt(preferences.refreshInterval) || 15,
8+
use24Hour: preferences.timeFormat === "24h",
9+
};
10+
}

extensions/agenda/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ export interface Calendar {
55
name: string;
66
color: Color;
77
}
8+
9+
export interface Preferences {
10+
refreshInterval: string;
11+
timeFormat: "12h" | "24h";
12+
}

extensions/agenda/src/upcoming-events.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { getPreferenceValues } from "@vicinae/api";
21
import { useCalendarData } from "./hooks/useCalendarData";
32
import { useCalendarFilter } from "./hooks/useCalendarFilter";
3+
import { usePreferences } from "./hooks/usePreferences";
44
import { UpcomingEvents } from "./components/UpcomingEvents";
55

66
export default function Command() {
7-
const preferences = getPreferenceValues();
8-
const refreshInterval = parseInt(preferences.refreshInterval) || 15;
7+
const { refreshInterval } = usePreferences();
98

109
const {
1110
calendars,

extensions/agenda/src/utils/calendar.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,23 @@ export const getCalendarName = (calendar: Calendar): string => {
3030
}
3131
};
3232

33+
/**
34+
* Parse YYYY-MM-DD string as local date components to avoid timezone shifts
35+
*/
36+
const parseDateString = (dateString: string): Date => {
37+
const [year, month, day] = dateString.split("-").map(Number);
38+
return new Date(year, month - 1, day);
39+
};
40+
3341
export const formatDate = (dateString: string) => {
34-
const date = new Date(dateString);
42+
const date = parseDateString(dateString);
3543
const today = new Date();
44+
today.setHours(0, 0, 0, 0);
3645
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
3746

38-
if (date.toDateString() === today.toDateString()) {
47+
if (date.getTime() === today.getTime()) {
3948
return "Today";
40-
} else if (date.toDateString() === tomorrow.toDateString()) {
49+
} else if (date.getTime() === tomorrow.getTime()) {
4150
return "Tomorrow";
4251
} else {
4352
return date.toLocaleDateString("en-US", {

extensions/agenda/src/utils/events.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,47 @@ export const isAllDayEvent = (start: Date, end: Date): boolean => {
1010
);
1111
};
1212

13-
export const getDisplayStart = (start: Date, isAllDay: boolean): string => {
13+
/**
14+
* Display time using UTC timezone because node-ical/rrule stores
15+
* local time values in the UTC position internally
16+
*/
17+
export const getDisplayStart = (
18+
start: Date,
19+
isAllDay: boolean,
20+
use24Hour: boolean = false
21+
): string => {
1422
return isAllDay
1523
? ""
1624
: start.toLocaleTimeString("en-US", {
1725
hour: "2-digit",
1826
minute: "2-digit",
19-
hour12: false,
27+
hour12: !use24Hour,
28+
timeZone: "UTC",
2029
});
2130
};
2231

2332
export const getDisplayEnd = (
2433
end: Date,
2534
isAllDay: boolean,
35+
use24Hour: boolean = false
2636
): string | undefined => {
2737
return isAllDay
2838
? undefined
2939
: end.toLocaleTimeString("en-US", {
3040
hour: "2-digit",
3141
minute: "2-digit",
32-
hour12: false,
42+
hour12: !use24Hour,
43+
timeZone: "UTC",
3344
});
34-
};
45+
};
46+
47+
/**
48+
* Get the date string for an event (YYYY-MM-DD format)
49+
* Uses UTC because node-ical stores local time values as UTC internally
50+
*/
51+
export const getLocalDateString = (date: Date): string => {
52+
const year = date.getUTCFullYear();
53+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
54+
const day = String(date.getUTCDate()).padStart(2, "0");
55+
return `${year}-${month}-${day}`;
56+
};

0 commit comments

Comments
 (0)