Skip to content

Commit fccc9db

Browse files
committed
Merge branch 'feat/ical-utils' into chore/all-my-stuffs
# Conflicts: # package.json # pnpm-lock.yaml # src/tools/index.ts
2 parents 73b6c3c + 6896d03 commit fccc9db

File tree

11 files changed

+545
-2
lines changed

11 files changed

+545
-2
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@
117117
"iarna-toml-esm": "^3.0.5",
118118
"ibantools": "^4.3.3",
119119
"js-base64": "^3.7.7",
120+
"ical-generator": "^8.0.0",
121+
"ical.js": "^2.1.0",
122+
"js-base64": "^3.7.6",
120123
"json5": "^2.2.3",
121124
"jwt-decode": "^3.1.2",
122125
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 54 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<script setup lang="ts">
2+
import slugify from '@sindresorhus/slugify';
3+
import ical, { ICalCalendarMethod } from 'ical-generator';
4+
import { Base64 } from 'js-base64';
5+
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
6+
7+
interface Event {
8+
startend: [number, number]
9+
summary?: string
10+
description?: string
11+
location?: string
12+
url?: string
13+
}
14+
15+
const events = ref<Array<Event>>([{
16+
startend: [Date.now(), Date.now()],
17+
summary: 'An event',
18+
}]);
19+
function deleteEvent(index: number) {
20+
if (events.value.length === 1) {
21+
return;
22+
}
23+
events.value.splice(index, 1);
24+
}
25+
function addEvent() {
26+
const now = Date.now();
27+
events.value.push({
28+
startend: [now, now + 3600 * 1000],
29+
summary: 'An event',
30+
});
31+
}
32+
33+
const output = computed(() => {
34+
try {
35+
const calendar = ical({ name: events.value[0]?.summary || 'unamed' });
36+
37+
// A method is required for outlook to display event as an invitation
38+
calendar.method(ICalCalendarMethod.REQUEST);
39+
40+
for (const event of events.value) {
41+
calendar.createEvent({
42+
start: new Date(event.startend[0]).toUTCString(),
43+
end: new Date(event.startend[0]).toUTCString(),
44+
summary: event.summary,
45+
description: event.description,
46+
location: event.location,
47+
url: event.url,
48+
});
49+
}
50+
51+
return { ical: calendar.toString() };
52+
}
53+
catch (e: any) {
54+
return { error: e.toString() };
55+
}
56+
});
57+
const outputBase64 = computed(() => Base64.encode(output.value?.ical || ''));
58+
const customOutputFileName = ref('');
59+
const outputFileName = computed(() => {
60+
if (customOutputFileName.value) {
61+
return customOutputFileName.value;
62+
}
63+
64+
return slugify(events.value[0]?.summary || 'unamed');
65+
});
66+
const { download } = useDownloadFileFromBase64(
67+
{
68+
source: outputBase64,
69+
filename: outputFileName,
70+
extension: 'ics',
71+
});
72+
</script>
73+
74+
<template>
75+
<div>
76+
<div mb-2 flex items-baseline gap-2>
77+
<c-input-text
78+
v-model:value="outputFileName"
79+
placeholder="Generated if empty"
80+
label="Output filename:"
81+
label-position="left"
82+
mb-2
83+
/>
84+
<c-button v-if="output.ical" @click="download">
85+
Download ICS
86+
</c-button>
87+
</div>
88+
89+
<c-alert v-if="output.error" mb-2>
90+
{{ output.error }}
91+
</c-alert>
92+
93+
<c-card v-for="(event, index) in events" :key="index" mb-2>
94+
<n-form-item label="Title:" label-placement="left">
95+
<n-input v-model:value="event.summary" :allow-input="(value:string) => !!value" />
96+
</n-form-item>
97+
<n-form-item label="Dates and hours:" label-placement="left">
98+
<n-date-picker v-model:value="event.startend" type="datetimerange" />
99+
</n-form-item>
100+
<c-input-text
101+
v-model:value="event.description"
102+
label="Description"
103+
placeholder="Put a description here"
104+
multiline
105+
rows="2"
106+
mb-2
107+
/>
108+
<c-input-text
109+
v-model:value="event.url"
110+
label="Url:"
111+
label-position="left"
112+
placeholder="Put an url here"
113+
mb-2
114+
/>
115+
<div flex justify-center>
116+
<c-button @click="deleteEvent(index)">
117+
Delete
118+
</c-button>
119+
</div>
120+
</c-card>
121+
<div mt-2 flex justify-center>
122+
<c-button @click="addEvent">
123+
Add Event
124+
</c-button>
125+
</div>
126+
</div>
127+
</template>

src/tools/ical-generator/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { CalendarEvent } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'ICAL Generator',
6+
path: '/ical-generator',
7+
description: 'Generate ICAL/ICS file from event infos',
8+
keywords: ['ical', 'calendar', 'event', 'generator'],
9+
component: () => import('./ical-generator.vue'),
10+
icon: CalendarEvent,
11+
createdAt: new Date('2024-08-15'),
12+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { mergeIcals } from './ical-merger.service';
3+
4+
describe('ical-merger', () => {
5+
describe('mergeIcals', () => {
6+
it('merge correctly', () => {
7+
expect(mergeIcals([
8+
`BEGIN:VCALENDAR
9+
PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
10+
VERSION:2.0
11+
BEGIN:VEVENT
12+
DTSTAMP:19960704T120000Z
13+
14+
ORGANIZER:mailto:[email protected]
15+
DTSTART:19960918T143000Z
16+
DTEND:19960920T220000Z
17+
STATUS:CONFIRMED
18+
CATEGORIES:CONFERENCE
19+
SUMMARY:Networld+Interop Conference
20+
DESCRIPTION:Networld+Interop Conference
21+
and Exhibit\\nAtlanta World Congress Center\\n
22+
Atlanta\\, Georgia
23+
END:VEVENT
24+
END:VCALENDAR`,
25+
`BEGIN:VCALENDAR
26+
METHOD:xyz
27+
VERSION:2.0
28+
PRODID:-//ABC Corporation//NONSGML My Product//EN
29+
BEGIN:VEVENT
30+
DTSTAMP:19970324T120000Z
31+
SEQUENCE:0
32+
33+
ORGANIZER:mailto:[email protected]
34+
ATTENDEE;RSVP=TRUE:mailto:[email protected]
35+
DTSTART:19970324T123000Z
36+
DTEND:19970324T210000Z
37+
CATEGORIES:MEETING,PROJECT
38+
CLASS:PUBLIC
39+
SUMMARY:Calendaring Interoperability Planning Meeting
40+
DESCRIPTION:Discuss how we can test c&s interoperability\\n
41+
using iCalendar and other IETF standards.
42+
LOCATION:LDB Lobby
43+
ATTACH;FMTTYPE=application/postscript:ftp://example.com/pub/
44+
conf/bkgrnd.ps
45+
END:VEVENT
46+
END:VCALENDAR`,
47+
])).to.eq(
48+
`BEGIN:VCALENDAR
49+
PRODID:it-tools-ical-merger
50+
VERSION:1.0
51+
BEGIN:VEVENT
52+
DTSTAMP:19960704T120000Z
53+
54+
ORGANIZER:mailto:[email protected]
55+
DTSTART:19960918T143000Z
56+
DTEND:19960920T220000Z
57+
STATUS:CONFIRMED
58+
CATEGORIES:CONFERENCE
59+
SUMMARY:Networld+Interop Conference
60+
DESCRIPTION:Networld+Interop Conference and Exhibit\\nAtlanta World Congress
61+
Center\\nAtlanta\\, Georgia
62+
END:VEVENT
63+
BEGIN:VEVENT
64+
DTSTAMP:19970324T120000Z
65+
SEQUENCE:0
66+
67+
ORGANIZER:mailto:[email protected]
68+
ATTENDEE;RSVP=TRUE:mailto:[email protected]
69+
DTSTART:19970324T123000Z
70+
DTEND:19970324T210000Z
71+
CATEGORIES:MEETING,PROJECT
72+
CLASS:PUBLIC
73+
SUMMARY:Calendaring Interoperability Planning Meeting
74+
DESCRIPTION:Discuss how we can test c&s interoperability\\nusing iCalendar a
75+
nd other IETF standards.
76+
LOCATION:LDB Lobby
77+
ATTACH;FMTTYPE=application/postscript:ftp://example.com/pub/conf/bkgrnd.ps
78+
END:VEVENT
79+
END:VCALENDAR`.replace(/\n/g, '\r\n'));
80+
});
81+
});
82+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import ICAL from 'ical.js';
2+
3+
export function mergeIcals(inputs: Array<string>, options: {
4+
calname?: string
5+
timezone?: string
6+
caldesc?: string
7+
} = {}) {
8+
let calendar;
9+
for (const input of inputs) {
10+
try {
11+
const jcal = ICAL.parse(input);
12+
const cal = new ICAL.Component(jcal);
13+
14+
if (!calendar) {
15+
calendar = cal;
16+
calendar.updatePropertyWithValue('prodid', 'it-tools-ical-merger');
17+
calendar.updatePropertyWithValue('version', '1.0');
18+
19+
if (options.calname) {
20+
calendar.updatePropertyWithValue('x-wr-calname', options.calname);
21+
}
22+
if (options.timezone) {
23+
calendar.updatePropertyWithValue('x-wr-timezone', options.timezone);
24+
}
25+
if (options.caldesc) {
26+
calendar.updatePropertyWithValue('x-wr-caldesc', options.caldesc);
27+
}
28+
}
29+
else {
30+
for (const vevent of cal.getAllSubcomponents('vevent')) {
31+
calendar.addSubcomponent(vevent);
32+
}
33+
}
34+
}
35+
catch (e) {
36+
throw new Error(`Failed to merge: ${e}\n\nWith input: ${input}`);
37+
}
38+
}
39+
40+
if (!calendar) {
41+
throw new Error('No icals parsed successfully');
42+
}
43+
44+
return calendar.toString();
45+
}

0 commit comments

Comments
 (0)