Skip to content

Commit 5dd136b

Browse files
committed
URL was missing from some calendar formats
We now prepend it to the description for the formats in question.
1 parent d830850 commit 5dd136b

2 files changed

Lines changed: 125 additions & 8 deletions

File tree

src/composables/__tests__/useCalendarLinks.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ describe('useCalendarLinks', () => {
3939
})
4040
expect(googleUrl.value).toContain('Event')
4141
})
42+
43+
it('includes url in details when no description', () => {
44+
const { googleUrl } = useCalendarLinks({
45+
title: ref('Test Event'),
46+
startTs: ref(START_TS),
47+
endTs: ref(null),
48+
url: ref('https://example.com'),
49+
})
50+
expect(googleUrl.value).toContain('example.com')
51+
})
52+
53+
it('prepends url before description in details', () => {
54+
const { googleUrl } = useCalendarLinks({
55+
title: ref('Test Event'),
56+
startTs: ref(START_TS),
57+
endTs: ref(null),
58+
description: ref('A description'),
59+
url: ref('https://example.com'),
60+
})
61+
const details = new URL(googleUrl.value).searchParams.get('details')!
62+
expect(details.indexOf('example.com')).toBeLessThan(details.indexOf('A description'))
63+
})
4264
})
4365

4466
describe('Outlook.com URL', () => {
@@ -82,6 +104,28 @@ describe('useCalendarLinks', () => {
82104
expect(outlookUrl.value).toContain('location=Amsterdam')
83105
})
84106

107+
it('includes url in body when no description', () => {
108+
const { outlookUrl } = useCalendarLinks({
109+
title: ref('Test Event'),
110+
startTs: ref(START_TS),
111+
endTs: ref(null),
112+
url: ref('https://example.com'),
113+
})
114+
expect(outlookUrl.value).toContain('example.com')
115+
})
116+
117+
it('prepends url before description in body', () => {
118+
const { outlookUrl } = useCalendarLinks({
119+
title: ref('Test Event'),
120+
startTs: ref(START_TS),
121+
endTs: ref(null),
122+
description: ref('A description'),
123+
url: ref('https://example.com'),
124+
})
125+
const body = new URL(outlookUrl.value).searchParams.get('body')!
126+
expect(body.indexOf('example.com')).toBeLessThan(body.indexOf('A description'))
127+
})
128+
85129
it('points to outlook.live.com', () => {
86130
const { outlookUrl } = useCalendarLinks({
87131
title: ref('Test Event'),
@@ -114,6 +158,30 @@ describe('useCalendarLinks', () => {
114158
})
115159
})
116160

161+
describe('Yahoo Calendar URL', () => {
162+
it('includes url in desc when no description', () => {
163+
const { yahooUrl } = useCalendarLinks({
164+
title: ref('Test Event'),
165+
startTs: ref(START_TS),
166+
endTs: ref(null),
167+
url: ref('https://example.com'),
168+
})
169+
expect(yahooUrl.value).toContain('example.com')
170+
})
171+
172+
it('prepends url before description in desc', () => {
173+
const { yahooUrl } = useCalendarLinks({
174+
title: ref('Test Event'),
175+
startTs: ref(START_TS),
176+
endTs: ref(null),
177+
description: ref('A description'),
178+
url: ref('https://example.com'),
179+
})
180+
const desc = new URL(yahooUrl.value).searchParams.get('desc')!
181+
expect(desc.indexOf('example.com')).toBeLessThan(desc.indexOf('A description'))
182+
})
183+
})
184+
117185
describe('ICS content', () => {
118186
it('contains VCALENDAR wrapper', () => {
119187
const { icsContent } = useCalendarLinks({
@@ -152,6 +220,30 @@ describe('useCalendarLinks', () => {
152220
expect(icsContent.value).toContain('SUMMARY:My Event')
153221
})
154222

223+
it('includes URL property when url is provided', () => {
224+
const { icsContent } = useCalendarLinks({
225+
title: ref('Test Event'),
226+
startTs: ref(START_TS),
227+
endTs: ref(null),
228+
url: ref('https://example.com'),
229+
})
230+
expect(icsContent.value).toContain('URL:https://example.com')
231+
})
232+
233+
it('does not duplicate url in DESCRIPTION when both are provided', () => {
234+
const { icsContent } = useCalendarLinks({
235+
title: ref('Test Event'),
236+
startTs: ref(START_TS),
237+
endTs: ref(null),
238+
description: ref('A description'),
239+
url: ref('https://example.com'),
240+
})
241+
expect(icsContent.value).toContain('URL:https://example.com')
242+
expect(icsContent.value).toContain('DESCRIPTION:A description')
243+
// url should appear exactly once (in URL: property, not also in DESCRIPTION)
244+
expect(icsContent.value.split('example.com').length - 1).toBe(1)
245+
})
246+
155247
it('works across midnight (next-day end time)', () => {
156248
const { icsContent } = useCalendarLinks({
157249
title: ref('Late Event'),

src/composables/useCalendarLinks.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ function toUtcIso(date: Date): string {
1717
return date.toISOString().replace(/\.\d{3}/, '')
1818
}
1919

20+
// Google/Yahoo/Outlook have no dedicated URL field — prepend it before the description.
21+
function descriptionWithUrl(description?: string, url?: string): string | undefined {
22+
if (!url) return description || undefined
23+
return description ? `${url}\n\n${description}` : url
24+
}
25+
2026
function outlookUrl(params: URLSearchParams, base: string): string {
2127
return `${base}?${params.toString()}`
2228
}
@@ -27,6 +33,7 @@ function buildOutlookParams(
2733
title: string,
2834
description?: string,
2935
location?: string,
36+
url?: string,
3037
): URLSearchParams {
3138
const p = new URLSearchParams({
3239
path: '/calendar/action/compose',
@@ -36,39 +43,56 @@ function buildOutlookParams(
3643
subject: title,
3744
allday: 'false',
3845
})
39-
if (description) p.set('body', description)
46+
const body = descriptionWithUrl(description, url)
47+
if (body) p.set('body', body)
4048
if (location) p.set('location', location)
4149
return p
4250
}
4351

4452
export function useCalendarLinks(opts: CalendarLinkOptions) {
4553
const startDate = computed(() => new Date(opts.startTs.value * 1000))
4654

47-
const event = computed((): CalendarEvent => ({
55+
const timingFields = computed(() =>
56+
opts.endTs.value !== null
57+
? { end: new Date(opts.endTs.value * 1000) }
58+
: { duration: [1, 'hour'] as [number, 'hour'] },
59+
)
60+
61+
// ICS has a native URL property — keep url as a separate field.
62+
const icsEvent = computed((): CalendarEvent => ({
4863
title: opts.title.value || 'Event',
4964
start: startDate.value,
50-
...(opts.endTs.value !== null
51-
? { end: new Date(opts.endTs.value * 1000) }
52-
: { duration: [1, 'hour'] as [number, 'hour'] }),
65+
...timingFields.value,
5366
...(opts.description?.value ? { description: opts.description.value } : {}),
5467
...(opts.location?.value ? { location: opts.location.value } : {}),
5568
...(opts.url?.value ? { url: opts.url.value } : {}),
5669
}))
5770

71+
// Google/Yahoo have no dedicated URL field — append it to description.
72+
const webEvent = computed((): CalendarEvent => ({
73+
title: opts.title.value || 'Event',
74+
start: startDate.value,
75+
...timingFields.value,
76+
...(descriptionWithUrl(opts.description?.value, opts.url?.value)
77+
? { description: descriptionWithUrl(opts.description?.value, opts.url?.value) }
78+
: {}),
79+
...(opts.location?.value ? { location: opts.location.value } : {}),
80+
}))
81+
5882
const endDate = computed(() =>
5983
opts.endTs.value !== null
6084
? new Date(opts.endTs.value * 1000)
6185
: new Date(opts.startTs.value * 1000 + 3600_000),
6286
)
6387

64-
const googleUrl = computed(() => google(event.value))
65-
const yahooUrl = computed(() => yahoo(event.value))
88+
const googleUrl = computed(() => google(webEvent.value))
89+
const yahooUrl = computed(() => yahoo(webEvent.value))
6690

6791
// ics() returns a data URI — decode it to get the raw ICS string so the
6892
// component can create a Blob URL, which browsers open in Calendar.app
6993
// rather than rendering as text.
7094
const icsContent = computed(() => {
71-
const dataUri = ics(event.value)
95+
const dataUri = ics(icsEvent.value)
7296
return decodeURIComponent(dataUri.replace('data:text/calendar;charset=utf8,', ''))
7397
})
7498

@@ -79,6 +103,7 @@ export function useCalendarLinks(opts: CalendarLinkOptions) {
79103
opts.title.value || 'Event',
80104
opts.description?.value,
81105
opts.location?.value,
106+
opts.url?.value,
82107
),
83108
)
84109

0 commit comments

Comments
 (0)