@dytsou/calendar-build 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 dytsou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,32 +1,96 @@
1
1
  # Calendar App
2
2
 
3
- A simple calendar application that displays multiple Google Calendars in a single view.
3
+ A simple calendar application that displays multiple calendar feeds using Open Web Calendar.
4
+
5
+ ## Features
6
+
7
+ - Display multiple calendar feeds simultaneously
8
+ - Dark theme with customizable styling
9
+ - Full-height responsive design
10
+ - URL parameters for view mode and date selection
11
+ - Clean interface without menu buttons or navigation controls
4
12
 
5
13
  ## Setup
6
14
 
15
+ ### 1. Configure Cloudflare Worker Secrets
16
+
17
+ Calendar URLs are now managed via Cloudflare Worker secrets for better security and centralized management.
18
+
19
+ **Set up your Cloudflare Worker secrets:**
20
+
21
+ ```bash
22
+ # Set calendar URLs (comma-separated, can be plain or fernet:// encrypted)
23
+ wrangler secret put CALENDAR_URL
24
+ # Enter: https://calendar.google.com/calendar/ical/example%40gmail.com/public/basic.ics,https://calendar.google.com/calendar/ical/another%40gmail.com/public/basic.ics
25
+
26
+ # Set encryption key (if using fernet:// encrypted URLs)
27
+ wrangler secret put ENCRYPTION_KEY
28
+ # Enter your Fernet encryption key (base64url-encoded 32-byte key)
29
+ # Generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''))"
30
+ ```
31
+
32
+ **Getting Google Calendar iCal URLs:**
33
+ - Go to your Google Calendar settings
34
+ - Find the calendar you want to share
35
+ - Click "Integrate calendar" or "Get shareable link"
36
+ - Copy the "Public URL to iCal format" link
37
+ - URL-encode special characters (e.g., `@` becomes `%40`)
38
+
39
+ ### 2. Configure Local Development (Optional)
40
+
7
41
  1. Copy `.env.example` to `.env`:
42
+
8
43
  ```bash
9
44
  cp .env.example .env
10
45
  ```
11
46
 
12
- 2. Edit `.env` and add your calendar sources (comma-separated):
47
+ 2. Edit `.env` and set your Cloudflare Worker URL (optional):
48
+
13
49
  ```
14
- CALENDAR_SOURCES=calendar1,calendar2,calendar3
50
+ WORKER_URL=https://cal-proxy.yourdomain.com
15
51
  ```
16
52
 
17
- 3. Build the HTML file:
18
- ```bash
19
- # Option 1: Using npm/pnpm (after installing)
20
- pnpm install
21
- pnpm run build
22
- # or use the global command if installed globally
23
- calendar-build
24
-
25
- # Option 2: Direct execution
26
- node build.js
27
- ```
53
+ If not set, defaults to `https://open-web-calendar.hosted.quelltext.eu`
54
+
55
+ ### 3. Build the HTML File
28
56
 
29
- 4. Open `index.html` in your browser.
57
+ ```bash
58
+ # Option 1: Using npm/pnpm (after installing)
59
+ pnpm install
60
+ pnpm run build
61
+ # or use the global command if installed globally
62
+ calendar-build
63
+
64
+ # Option 2: Direct execution
65
+ node scripts/build.js
66
+ ```
67
+
68
+ ### 4. Deploy and Open
69
+
70
+ - Deploy `index.html` to your hosting service (GitHub Pages, etc.)
71
+ - Open the deployed page in your browser
72
+ - The Cloudflare Worker will automatically add calendar URLs from secrets to all requests
73
+
74
+ ## URL Parameters
75
+
76
+ The calendar supports URL parameters for navigation:
77
+
78
+ - **`?mode=month`** - Show month view
79
+ - **`?mode=week`** - Show week view (default)
80
+ - **`?mode=day`** - Show day view
81
+ - **`?date=YYYYMMDD`** - Navigate to a specific date (e.g., `?date=20250115`)
82
+ - **`?theme=dark`** - Force dark theme
83
+ - **`?theme=light`** - Force light theme
84
+
85
+ **Theme:** The calendar uses your browser's color scheme preference by default. You can override it using the `?theme=` URL parameter.
86
+
87
+ **Examples:**
88
+
89
+ - `index.html?mode=month` - Month view (uses browser theme preference)
90
+ - `index.html?mode=week&date=20250115` - Week view for the week containing January 15, 2025
91
+ - `index.html?mode=day&date=20250320` - Day view for March 20, 2025
92
+ - `index.html?theme=light` - Light theme with default week view
93
+ - `index.html?mode=month&theme=dark` - Month view with dark theme
30
94
 
31
95
  ## Install Package
32
96
 
@@ -38,6 +102,7 @@ This package is published to **both registries**:
38
102
  ### Installation from npmjs (Default - Recommended)
39
103
 
40
104
  **Global installation:**
105
+
41
106
  ```bash
42
107
  npm install -g @dytsou/calendar-build
43
108
  # or
@@ -45,11 +110,13 @@ pnpm install -g @dytsou/calendar-build
45
110
  ```
46
111
 
47
112
  Then use anywhere:
113
+
48
114
  ```bash
49
115
  calendar-build
50
116
  ```
51
117
 
52
118
  **Local installation:**
119
+
53
120
  ```bash
54
121
  npm install @dytsou/calendar-build
55
122
  # or
@@ -57,6 +124,7 @@ pnpm install @dytsou/calendar-build
57
124
  ```
58
125
 
59
126
  Then use:
127
+
60
128
  ```bash
61
129
  npx calendar-build
62
130
  # or
@@ -77,12 +145,14 @@ Create or edit `.npmrc` file in your home directory:
77
145
  ```
78
146
 
79
147
  **2. Get your GitHub token:**
148
+
80
149
  1. Go to https://github.com/settings/tokens
81
150
  2. Click "Generate new token" → "Generate new token (classic)"
82
151
  3. Select `read:packages` permission
83
152
  4. Copy the token and replace `YOUR_GITHUB_TOKEN` in `.npmrc`
84
153
 
85
154
  **3. Install:**
155
+
86
156
  ```bash
87
157
  npm install -g @dytsou/calendar-build
88
158
  # or
@@ -91,8 +161,95 @@ pnpm install -g @dytsou/calendar-build
91
161
 
92
162
  ## Development
93
163
 
94
- - `index.html.template` - Template file with placeholder for calendar sources
95
- - `build.js` - Build script that injects calendar sources from `.env`
164
+ ### Project Structure
165
+
166
+ - `index.html.template` - Template file with placeholders for calendar URLs
167
+ - `scripts/build.js` - Build script that injects calendar URLs from `.env` and updates year in LICENSE
168
+ - `scripts/encrypt-urls.js` - Helper script to encrypt calendar URLs using Fernet
96
169
  - `.env` - Local environment file (not committed to git)
97
170
  - `.env.example` - Example environment file template
98
171
 
172
+ ### Scripts
173
+
174
+ - `pnpm run build` - Build the HTML file from template
175
+ - `pnpm format` - Format code with Prettier
176
+ - `pnpm format:check` - Check code formatting
177
+
178
+ ### Build Process
179
+
180
+ The build script:
181
+
182
+ 1. Reads `WORKER_URL` from `.env` (optional, defaults to open-web-calendar)
183
+ 2. Replaces `{{WORKER_URL}}` placeholder in the template
184
+ 3. Updates `{{YEAR}}` placeholder with current year in LICENSE and HTML
185
+ 4. Generates `index.html` ready for deployment
186
+
187
+ **Note:** Calendar URLs are now managed via Cloudflare Worker secrets (`CALENDAR_URL`), not in `.env` or the HTML file. The worker automatically adds them to all calendar requests.
188
+
189
+ ## Cloudflare Worker Setup
190
+
191
+ This project uses a Cloudflare Worker to manage calendar URLs securely. Calendar URLs are stored as Cloudflare Worker secrets, keeping them out of the HTML and GitHub secrets.
192
+
193
+ ### Setup Instructions
194
+
195
+ 1. **Copy wrangler.toml.example to wrangler.toml:**
196
+ ```bash
197
+ cp wrangler.toml.example wrangler.toml
198
+ ```
199
+ Then edit `wrangler.toml` and customize it for your environment (e.g., add custom domain routes).
200
+
201
+ 2. **Install Wrangler CLI:**
202
+ ```bash
203
+ npm install -g wrangler
204
+ # or
205
+ pnpm add -g wrangler
206
+ ```
207
+
208
+ 3. **Login to Cloudflare:**
209
+ ```bash
210
+ wrangler login
211
+ ```
212
+
213
+ 4. **Set Calendar URLs Secret:**
214
+ ```bash
215
+ wrangler secret put CALENDAR_URL
216
+ ```
217
+ When prompted, enter your calendar URLs (comma-separated):
218
+ ```
219
+ https://calendar.google.com/calendar/ical/example%40gmail.com/public/basic.ics,https://calendar.google.com/calendar/ical/another%40gmail.com/public/basic.ics
220
+ ```
221
+
222
+ **Note:** URLs can be plain or `fernet://` encrypted. If using encrypted URLs, you'll also need to set `ENCRYPTION_KEY`.
223
+
224
+ 5. **Set Encryption Key Secret (if using fernet:// encrypted URLs):**
225
+ ```bash
226
+ wrangler secret put ENCRYPTION_KEY
227
+ ```
228
+ When prompted, enter your Fernet encryption key (base64url-encoded 32-byte key).
229
+
230
+ Generate one with:
231
+ ```bash
232
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''))"
233
+ ```
234
+
235
+ 6. **Deploy the Worker:**
236
+ ```bash
237
+ wrangler deploy
238
+ ```
239
+
240
+ 6. **Update your .env file:**
241
+ After deployment, update the `WORKER_URL` in your `.env` file with your worker URL:
242
+ ```
243
+ WORKER_URL=https://your-worker.your-subdomain.workers.dev
244
+ # or if using custom domain:
245
+ WORKER_URL=https://your-domain.com
246
+ ```
247
+
248
+ 7. **Rebuild your project:**
249
+ ```bash
250
+ pnpm run build
251
+ ```
252
+
253
+ ## License
254
+
255
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -1,13 +1,30 @@
1
1
  <!DOCTYPE html>
2
+
3
+ <!--
4
+ MIT License
5
+ Copyright (c) {{YEAR}} dytsou
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ -->
10
+
2
11
  <html lang="zh-TW">
3
12
 
4
13
  <head>
5
14
  <meta charset="UTF-8">
6
15
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <base href="/cal/">
7
17
  <title>My Calendar</title>
8
18
  <link rel="icon" type="image/png" href="asset/favicon.png">
9
19
 
10
20
  <style>
21
+ html, body {
22
+ height: 100%;
23
+ margin: 0;
24
+ padding: 0;
25
+ overflow: hidden;
26
+ }
27
+
11
28
  body {
12
29
  transition: opacity ease-in 0.2s;
13
30
  }
@@ -22,75 +39,309 @@
22
39
  iframe {
23
40
  border: solid 1px #777;
24
41
  width: 100%;
25
- height: 100vh;
42
+ height: calc(100vh - 25px);
43
+ }
44
+
45
+ /* Hide error messages that might expose calendar URLs */
46
+ iframe {
47
+ /* Note: Cross-origin iframe content cannot be styled directly */
26
48
  }
27
49
  </style>
28
50
  </head>
29
51
 
30
52
  <body unresolved>
31
53
 
32
- <iframe id="calendarFrame" frameborder="0" scrolling="no"></iframe>
33
-
34
- <script>
35
- const calendarBaseURL = "https://calendar.google.com/calendar/embed?";
36
- const searchParams = new URLSearchParams(window.location.search);
37
- const calendarParams = new URLSearchParams({
38
- height: 600,
39
- wkst: 1,
40
- ctz: "Asia/Taipei",
41
- showPrint: 0,
42
- showTitle: 0,
43
- showDate: 0,
44
- showNav: 0,
45
- showCalendars: 0,
46
- showTabs: 0,
47
- mode: "WEEK"
48
- });
49
-
50
- const calendars = {{CALENDAR_SOURCES}};
51
-
52
- const defaultColor = "%23A1B3FF";
53
-
54
- const parseDatesRange = (value) => {
55
- if (!value) return null;
56
- const match = /^(\d{8})\/(\d{8})$/.exec(value);
57
- if (!match) return null;
58
- const [, start, end] = match;
54
+ <iframe id="open-web-calendar"
55
+ style="background:url('https://raw.githubusercontent.com/niccokunzmann/open-web-calendar/master/static/img/loaders/circular-loader.gif') center center no-repeat;"
56
+ sandbox="allow-scripts allow-same-origin allow-downloads"
57
+ allowTransparency="true" scrolling="no"
58
+ frameborder="0" width="100%"></iframe>
59
59
 
60
- // Basic validation to ensure chronological order.
61
- if (start > end) return null;
62
-
63
- return `${start}/${end}`;
64
- };
65
-
66
- const datesFromQuery = parseDatesRange(searchParams.get("dates"));
67
- if (datesFromQuery) {
68
- calendarParams.set("dates", datesFromQuery);
69
- }
60
+ <p style="position: absolute; bottom: 2px; left: 0; right: 0; text-align: center; font-size: 9px; color: #ffffff; background-color: #000000; margin: 0; padding: 4px; pointer-events: none;">Copyright (c) {{YEAR}} dytsou</p>
70
61
 
71
- const defaultMode = "WEEK";
72
- const allowedModes = new Set(["MONTH", "WEEK", "AGENDA"]);
73
- const requestedMode = searchParams.get("mode");
62
+ <script>
63
+ // Filter console logs to hide calendar URLs and sensitive information
64
+ (function() {
65
+ const originalError = console.error;
66
+ const originalLog = console.log;
67
+ const originalWarn = console.warn;
68
+ const originalInfo = console.info;
69
+
70
+ // Patterns to detect and sanitize calendar URLs in error messages
71
+ const urlPatterns = [
72
+ /https?:\/\/[^\s"']+\.ics/gi,
73
+ /https?:\/\/calendar\.google\.com\/calendar\/ical\/[^\s"']+/gi,
74
+ /%40[^\s"']+/gi, // URL-encoded @ symbols
75
+ /@[^\s"']+\.ics/gi, // @ symbols in URLs
76
+ ];
77
+
78
+ function sanitizeMessage(message) {
79
+ if (typeof message !== 'string') {
80
+ return message;
81
+ }
82
+ let sanitized = message;
83
+ urlPatterns.forEach(pattern => {
84
+ sanitized = sanitized.replace(pattern, '[Calendar URL hidden]');
85
+ });
86
+ return sanitized;
87
+ }
88
+
89
+ function sanitizeArgs(args) {
90
+ return args.map(arg => {
91
+ if (typeof arg === 'string') {
92
+ return sanitizeMessage(arg);
93
+ } else if (arg && typeof arg === 'object') {
94
+ // Try to sanitize object string representations
95
+ try {
96
+ const str = JSON.stringify(arg);
97
+ const sanitized = sanitizeMessage(str);
98
+ if (sanitized !== str) {
99
+ return JSON.parse(sanitized);
100
+ }
101
+ } catch (e) {
102
+ // If JSON parsing fails, return original
103
+ }
104
+ }
105
+ return arg;
106
+ });
107
+ }
108
+
109
+ console.error = function(...args) {
110
+ // Check if message contains calendar URL patterns
111
+ const message = args.join(' ');
112
+ if (urlPatterns.some(pattern => pattern.test(message))) {
113
+ // Replace with sanitized version
114
+ const sanitized = sanitizeArgs(args);
115
+ originalError.apply(console, sanitized);
116
+ } else {
117
+ originalError.apply(console, args);
118
+ }
119
+ };
120
+
121
+ // Helper function to check if args contain calendar info
122
+ function containsCalendarInfo(args) {
123
+ // Check string messages
124
+ const message = args.map(arg => {
125
+ if (typeof arg === 'string') {
126
+ return arg;
127
+ } else if (arg && typeof arg === 'object') {
128
+ try {
129
+ return JSON.stringify(arg);
130
+ } catch (e) {
131
+ return String(arg);
132
+ }
133
+ }
134
+ return String(arg);
135
+ }).join(' ');
136
+
137
+ // Check for calendar info patterns
138
+ if (message.includes('Calendar Info:') ||
139
+ message.includes('calendars:') ||
140
+ message.includes('calendar_index:') ||
141
+ message.includes('url_index:') ||
142
+ message.includes('HTTPError') ||
143
+ message.includes('Error in ConvertToCalendars')) {
144
+ return true;
145
+ }
146
+
147
+ // Check if any arg is an object with calendar-related properties
148
+ for (const arg of args) {
149
+ if (arg && typeof arg === 'object') {
150
+ const keys = Object.keys(arg);
151
+ if (keys.includes('calendars') ||
152
+ keys.includes('calendar_index') ||
153
+ keys.includes('url_index') ||
154
+ (arg.calendars && Array.isArray(arg.calendars))) {
155
+ return true;
156
+ }
157
+ }
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ console.log = function(...args) {
164
+ // Filter out calendar-related logs completely
165
+ if (containsCalendarInfo(args) ||
166
+ urlPatterns.some(pattern => {
167
+ const message = args.map(a => String(a)).join(' ');
168
+ return pattern.test(message);
169
+ })) {
170
+ // Don't log calendar info at all
171
+ return;
172
+ }
173
+ originalLog.apply(console, args);
174
+ };
175
+
176
+ console.warn = function(...args) {
177
+ const message = args.map(a => String(a)).join(' ');
178
+ if (urlPatterns.some(pattern => pattern.test(message))) {
179
+ const sanitized = sanitizeArgs(args);
180
+ originalWarn.apply(console, sanitized);
181
+ } else {
182
+ originalWarn.apply(console, args);
183
+ }
184
+ };
185
+
186
+ console.info = function(...args) {
187
+ // Filter out calendar-related info completely
188
+ if (containsCalendarInfo(args) ||
189
+ urlPatterns.some(pattern => {
190
+ const message = args.map(a => String(a)).join(' ');
191
+ return pattern.test(message);
192
+ })) {
193
+ // Don't log calendar info at all
194
+ return;
195
+ }
196
+ originalInfo.apply(console, args);
197
+ };
198
+ })();
199
+
200
+ // Get mode from URL parameter (e.g., ?mode=month, ?mode=week, ?mode=day)
201
+ const urlParams = new URLSearchParams(window.location.search);
202
+ const requestedMode = urlParams.get('mode');
203
+ const requestedDate = urlParams.get('date');
204
+ const requestedTheme = urlParams.get('theme');
205
+
206
+ // Valid modes and their mapping to open-web-calendar tab values
207
+ const validModes = ['month', 'week', 'day'];
208
+ const defaultMode = 'week';
209
+
210
+ // Determine the tab to use
211
+ let selectedTab = defaultMode;
74
212
  if (requestedMode) {
75
- const normalizedMode = requestedMode.toUpperCase();
76
- if (allowedModes.has(normalizedMode)) {
77
- calendarParams.set("mode", normalizedMode);
213
+ const normalizedMode = requestedMode.toLowerCase();
214
+ if (validModes.includes(normalizedMode)) {
215
+ selectedTab = normalizedMode;
216
+ }
217
+ }
218
+
219
+ // Determine the theme: URL parameter takes precedence, then browser preference
220
+ const validThemes = ['dark', 'light'];
221
+ let selectedTheme;
222
+ if (requestedTheme) {
223
+ const normalizedTheme = requestedTheme.toLowerCase();
224
+ if (validThemes.includes(normalizedTheme)) {
225
+ selectedTheme = normalizedTheme;
78
226
  } else {
79
- calendarParams.set("mode", defaultMode);
227
+ // Invalid theme parameter, fall back to browser preference
228
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
229
+ selectedTheme = prefersDark ? 'dark' : 'light';
230
+ }
231
+ } else {
232
+ // No theme parameter, use browser preference
233
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
234
+ selectedTheme = prefersDark ? 'dark' : 'light';
235
+ }
236
+
237
+ // Parse and validate date parameter (YYYYMMDD format)
238
+ let selectedDate = null;
239
+ if (requestedDate) {
240
+ // Validate YYYYMMDD format
241
+ const dateMatch = /^(\d{4})(\d{2})(\d{2})$/.exec(requestedDate);
242
+ if (dateMatch) {
243
+ const [, year, month, day] = dateMatch;
244
+ // Validate the date is actually valid
245
+ const dateObj = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
246
+ if (dateObj.getFullYear() == year &&
247
+ dateObj.getMonth() == parseInt(month) - 1 &&
248
+ dateObj.getDate() == day) {
249
+ // Convert YYYYMMDD to YYYY-MM-DD format for open-web-calendar
250
+ selectedDate = `${year}-${month}-${day}`;
251
+ }
80
252
  }
81
253
  }
254
+
255
+ // Calendar configuration
256
+ // Use Cloudflare Worker to decrypt fernet:// URLs server-side
257
+ // IMPORTANT: Must use /calendar.html so document.location.pathname is correct
258
+ // The calendar.js uses pathname.replace(/.html$/, ".json") for API calls
259
+ const calendarConfig = {
260
+ baseUrl: '{{WORKER_URL}}/calendar.html',
261
+ params: {
262
+ controls: '',
263
+ menu: 'false',
264
+ css: selectedTheme === 'dark'
265
+ ? '.dhx_scale_holder_now, .dhx_now .dhx_month_head, .dhx_now .dhx_month_body { background-color: #000000;}.dhx_month_body, .dhx_month_head, .dhx_cal_container { background-color: #000000; }.dhx_cal_navline, .dhx_cal_navline div, button[aria-label*="menu"], button[aria-label*="Menu"], .menu-button, [class*="menu"], [class*="hamburger"] { display: none !important; }'
266
+ : '.dhx_cal_navline, .dhx_cal_navline div, button[aria-label*="menu"], button[aria-label*="Menu"], .menu-button, [class*="menu"], [class*="hamburger"] { display: none !important; }',
267
+ event_url_geo: 'https://www.google.com/maps/@{lat},{lon},{zoom}z',
268
+ event_url_location: 'https://www.google.com/maps/search/{location}',
269
+ menu_shows_calendar_names: 'false',
270
+ menu_shows_description: 'false',
271
+ menu_shows_title: 'false',
272
+ skin: selectedTheme,
273
+ start_of_week: 'su',
274
+ 'style-event-status-cancelled': 'true',
275
+ 'style-event-status-confirmed': 'true',
276
+ 'style-event-status-tentative': 'true',
277
+ tab: selectedTab,
278
+ tabs: '',
279
+ target: '_self',
280
+ title: 'My Calendar'
281
+ }
282
+ };
283
+
284
+ // Add date parameter if provided
285
+ if (selectedDate) {
286
+ calendarConfig.params.date = selectedDate;
287
+ }
82
288
 
83
- calendars.forEach(calendar => {
84
- calendarParams.append("src", calendar);
85
- calendarParams.append("color", defaultColor);
86
- });
289
+ // Build calendar URL
290
+ // Note: Calendar URLs are stored in Cloudflare Worker secrets
291
+ // The worker automatically adds them to all requests
292
+ const buildCalendarUrl = (config) => {
293
+ const url = new URL(config.baseUrl);
294
+
295
+ // Add regular parameters
296
+ Object.entries(config.params).forEach(([key, value]) => {
297
+ if (value !== '' && value !== null) {
298
+ url.searchParams.append(key, value);
299
+ }
300
+ });
301
+
302
+ // Calendar URLs are handled by the Cloudflare Worker from secrets
303
+ // No need to pass them in the URL
304
+
305
+ return url.toString();
306
+ };
87
307
 
88
- document.getElementById("calendarFrame").src = calendarBaseURL + calendarParams.toString();
308
+ // Set iframe source
309
+ const iframe = document.getElementById('open-web-calendar');
310
+ iframe.src = buildCalendarUrl(calendarConfig);
311
+
312
+ // Intercept and filter error messages from iframe
313
+ // Note: This only works if iframe is same-origin
314
+ iframe.addEventListener('load', function() {
315
+ try {
316
+ // Try to access iframe content (only works if same-origin)
317
+ const iframeWindow = iframe.contentWindow;
318
+ if (iframeWindow) {
319
+ // Override console methods in iframe
320
+ const originalIframeError = iframeWindow.console.error;
321
+ const urlPatterns = [
322
+ /https?:\/\/[^\s"']+\.ics/gi,
323
+ /calendar\.google\.com\/calendar\/ical\/[^\s"']+/gi,
324
+ /%40[^\s"']+/gi,
325
+ ];
326
+
327
+ iframeWindow.console.error = function(...args) {
328
+ const message = args.join(' ');
329
+ if (urlPatterns.some(pattern => pattern.test(message))) {
330
+ // Don't log errors with calendar URLs
331
+ return;
332
+ }
333
+ originalIframeError.apply(iframeWindow.console, args);
334
+ };
335
+ }
336
+ } catch (e) {
337
+ // Cross-origin restriction - can't access iframe content
338
+ // Console filtering on parent page will still work
339
+ }
340
+ });
89
341
 
90
342
  window.onload = () => document.body.removeAttribute('unresolved');
91
343
  </script>
92
344
 
93
345
  </body>
94
346
 
95
- </html>
96
-
347
+ </html>
package/package.json CHANGED
@@ -1,19 +1,16 @@
1
1
  {
2
2
  "name": "@dytsou/calendar-build",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Build script for calendar HTML from template and environment variables",
5
- "main": "build.js",
5
+ "main": "scripts/build.js",
6
6
  "bin": {
7
- "calendar-build": "build.js"
7
+ "calendar-build": "scripts/build.js"
8
8
  },
9
9
  "files": [
10
- "build.js",
10
+ "scripts/build.js",
11
11
  "index.html.template",
12
12
  "README.md"
13
13
  ],
14
- "scripts": {
15
- "build": "node build.js"
16
- },
17
14
  "keywords": [
18
15
  "calendar",
19
16
  "build",
@@ -31,5 +28,19 @@
31
28
  "homepage": "https://github.com/dytsou/cal#readme",
32
29
  "engines": {
33
30
  "node": ">=12.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "browserify": "^17.0.1",
34
+ "prettier": "^3.7.4",
35
+ "wrangler": "4.54.0"
36
+ },
37
+ "dependencies": {
38
+ "fernet": "^0.3.3",
39
+ "glob": "13.0.0"
40
+ },
41
+ "scripts": {
42
+ "build": "node scripts/build.js",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check ."
34
45
  }
35
46
  }
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Fernet = require('fernet');
6
+ const { execSync } = require('child_process');
7
+
8
+ // Read template HTML
9
+ const templatePath = path.join(__dirname, '..', 'index.html.template');
10
+ if (!fs.existsSync(templatePath)) {
11
+ console.error('Error: index.html.template not found');
12
+ process.exit(1);
13
+ }
14
+
15
+ let html = fs.readFileSync(templatePath, 'utf-8');
16
+
17
+ // Always check for placeholders that need replacement
18
+ const hasPlaceholders = html.includes('{{CALENDAR_SOURCES}}') || html.includes('{{CALENDAR_URLS}}');
19
+ const needsWorkerUrl = html.includes('{{WORKER_URL}}');
20
+ let workerUrl = '';
21
+
22
+ // Always read .env for WORKER_URL if needed
23
+ if (needsWorkerUrl || hasPlaceholders) {
24
+ const envPath = path.join(__dirname, '..', '.env');
25
+ if (hasPlaceholders && !fs.existsSync(envPath)) {
26
+ console.error('Error: .env file not found (required for placeholders)');
27
+ process.exit(1);
28
+ }
29
+
30
+ if (fs.existsSync(envPath)) {
31
+ const envContent = fs.readFileSync(envPath, 'utf-8');
32
+ const envLines = envContent.split('\n');
33
+
34
+ // Parse environment variables
35
+ let calendarSources = [];
36
+ let calendarUrls = [];
37
+
38
+ for (const line of envLines) {
39
+ if (line.startsWith('CALENDAR_SOURCES=')) {
40
+ const value = line.substring('CALENDAR_SOURCES='.length).trim();
41
+ calendarSources = value
42
+ .split(',')
43
+ .map(s => s.trim())
44
+ .filter(s => s);
45
+ } else if (line.startsWith('CALENDAR_URL=')) {
46
+ const value = line.substring('CALENDAR_URL='.length).trim();
47
+ // Support both single URL and comma-separated multiple URLs
48
+ const urls = value
49
+ .split(',')
50
+ .map(s => s.trim())
51
+ .filter(s => s);
52
+ calendarUrls.push(...urls);
53
+ } else if (line.startsWith('WORKER_URL=')) {
54
+ workerUrl = line.substring('WORKER_URL='.length).trim();
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ if (hasPlaceholders) {
61
+
62
+ // Replace CALENDAR_SOURCES placeholder
63
+ if (html.includes('{{CALENDAR_SOURCES}}')) {
64
+ if (calendarSources.length === 0) {
65
+ console.error('Error: No calendar sources found in .env file');
66
+ process.exit(1);
67
+ }
68
+
69
+ const calendarArrayCode =
70
+ '[\n' + calendarSources.map(cal => ` "${cal}"`).join(',\n') + '\n ]';
71
+
72
+ html = html.replace('{{CALENDAR_SOURCES}}', calendarArrayCode);
73
+ console.log(
74
+ `āœ“ Replaced {{CALENDAR_SOURCES}} with ${calendarSources.length} calendar source(s)`
75
+ );
76
+ }
77
+
78
+ // Replace CALENDAR_URLS placeholder
79
+ if (html.includes('{{CALENDAR_URLS}}')) {
80
+ if (calendarUrls.length === 0) {
81
+ console.error('Error: CALENDAR_URL not found in .env file');
82
+ process.exit(1);
83
+ }
84
+
85
+ // Get encryption settings from .env
86
+ let encryptionMethod = '';
87
+ let encryptionKey = '';
88
+
89
+ for (const line of envLines) {
90
+ if (line.startsWith('ENCRYPTION_METHOD=')) {
91
+ encryptionMethod = line.substring('ENCRYPTION_METHOD='.length).trim().toLowerCase();
92
+ } else if (line.startsWith('ENCRYPTION_KEY=')) {
93
+ encryptionKey = line.substring('ENCRYPTION_KEY='.length).trim();
94
+ }
95
+ }
96
+
97
+ let calendarUrlsArrayCode;
98
+
99
+ if (encryptionMethod === 'fernet' && encryptionKey) {
100
+ // Use Fernet encryption
101
+ try {
102
+ const secret = new Fernet.Secret(encryptionKey);
103
+ const token = new Fernet.Token({
104
+ secret: secret,
105
+ ttl: 0, // No expiration
106
+ });
107
+
108
+ const encryptedUrls = calendarUrls.map(url => {
109
+ const encrypted = token.encode(url);
110
+ // Format as fernet:// protocol URL
111
+ return `fernet://${encrypted}`;
112
+ });
113
+
114
+ calendarUrlsArrayCode =
115
+ '[\n' + encryptedUrls.map(url => ` "${url}"`).join(',\n') + '\n ]';
116
+
117
+ html = html.replace('{{ENCRYPTION_METHOD}}', 'fernet');
118
+ html = html.replace('{{ENCRYPTION_KEY}}', encryptionKey);
119
+ console.log(
120
+ `āœ“ Replaced {{CALENDAR_URLS}} with ${encryptedUrls.length} Fernet-encrypted calendar URL(s)`
121
+ );
122
+
123
+ // Output encrypted URLs for copying to .env
124
+ console.log('\nšŸ“‹ Encrypted URLs (for .env file):');
125
+ console.log('CALENDAR_URL_ENCRYPTED=' + encryptedUrls.join(','));
126
+ console.log('\nOr individually:');
127
+ encryptedUrls.forEach((encrypted, index) => {
128
+ console.log(`# URL ${index + 1}: ${encrypted}`);
129
+ });
130
+ } catch (error) {
131
+ console.error('Error encrypting with Fernet:', error.message);
132
+ process.exit(1);
133
+ }
134
+ } else {
135
+ // No encryption - plain URLs
136
+ calendarUrlsArrayCode =
137
+ '[\n' + calendarUrls.map(url => ` "${url}"`).join(',\n') + '\n ]';
138
+ html = html.replace('{{ENCRYPTION_METHOD}}', 'none');
139
+ html = html.replace('{{ENCRYPTION_KEY}}', '');
140
+ html = html.replace('{{WORKER_URL}}', 'https://open-web-calendar.hosted.quelltext.eu/calendar.html');
141
+ console.log(
142
+ `āœ“ Replaced {{CALENDAR_URLS}} with ${calendarUrls.length} calendar URL(s) (no encryption)`
143
+ );
144
+ }
145
+
146
+ html = html.replace('{{CALENDAR_URLS}}', calendarUrlsArrayCode);
147
+
148
+ // Remove encryption key placeholder (not needed with Worker)
149
+ html = html.replace('{{ENCRYPTION_KEY}}', '');
150
+
151
+ // Clean up fernet-bundle.js if it exists (no longer needed with Worker)
152
+ const bundleFile = path.join(__dirname, '..', 'fernet-bundle.js');
153
+ if (fs.existsSync(bundleFile)) {
154
+ fs.unlinkSync(bundleFile);
155
+ console.log('āœ“ Removed fernet-bundle.js (using Cloudflare Worker instead)');
156
+ }
157
+ }
158
+ }
159
+
160
+ // Always replace WORKER_URL placeholder if it exists (regardless of other placeholders)
161
+ if (html.includes('{{WORKER_URL}}')) {
162
+ const finalWorkerUrl = workerUrl || process.env.WORKER_URL || 'https://open-web-calendar.hosted.quelltext.eu';
163
+ html = html.replace('{{WORKER_URL}}', finalWorkerUrl);
164
+ if (finalWorkerUrl !== 'https://open-web-calendar.hosted.quelltext.eu') {
165
+ console.log(`āœ“ Using Cloudflare Worker: ${finalWorkerUrl}`);
166
+ } else {
167
+ console.log('āœ“ Replaced {{WORKER_URL}} with default open-web-calendar URL');
168
+ }
169
+ }
170
+
171
+ // Get current year for placeholders
172
+ const currentYear = new Date().getFullYear().toString();
173
+
174
+ // Replace {{YEAR}} placeholder in HTML template
175
+ if (html.includes('{{YEAR}}')) {
176
+ html = html.replace(/\{\{YEAR\}\}/g, currentYear);
177
+ }
178
+
179
+ // Write built HTML
180
+ const outputPath = path.join(__dirname, '..', 'index.html');
181
+ fs.writeFileSync(outputPath, html, 'utf-8');
182
+
183
+ // Update LICENSE file with current year
184
+ const licensePath = path.join(__dirname, '..', 'LICENSE');
185
+ if (fs.existsSync(licensePath)) {
186
+ let licenseContent = fs.readFileSync(licensePath, 'utf-8');
187
+ if (licenseContent.includes('{{YEAR}}')) {
188
+ licenseContent = licenseContent.replace(/\{\{YEAR\}\}/g, currentYear);
189
+ fs.writeFileSync(licensePath, licenseContent, 'utf-8');
190
+ console.log(`āœ“ Updated LICENSE with year ${currentYear}`);
191
+ }
192
+ }
package/build.js DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- // Read .env file
7
- const envPath = path.join(__dirname, '.env');
8
- if (!fs.existsSync(envPath)) {
9
- console.error('Error: .env file not found');
10
- process.exit(1);
11
- }
12
-
13
- const envContent = fs.readFileSync(envPath, 'utf-8');
14
- const envLines = envContent.split('\n');
15
-
16
- // Parse CALENDAR_SOURCES from .env
17
- let calendarSources = [];
18
- for (const line of envLines) {
19
- if (line.startsWith('CALENDAR_SOURCES=')) {
20
- const value = line.substring('CALENDAR_SOURCES='.length).trim();
21
- calendarSources = value.split(',').map(s => s.trim()).filter(s => s);
22
- break;
23
- }
24
- }
25
-
26
- if (calendarSources.length === 0) {
27
- console.error('Error: No calendar sources found in .env file');
28
- process.exit(1);
29
- }
30
-
31
- // Read template HTML
32
- const templatePath = path.join(__dirname, 'index.html.template');
33
- if (!fs.existsSync(templatePath)) {
34
- console.error('Error: index.html.template not found');
35
- process.exit(1);
36
- }
37
-
38
- let html = fs.readFileSync(templatePath, 'utf-8');
39
-
40
- // Generate calendar array as JavaScript code
41
- const calendarArrayCode = '[\n' + calendarSources
42
- .map(cal => ` "${cal}"`)
43
- .join(',\n') + '\n ]';
44
-
45
- // Replace placeholder
46
- html = html.replace('{{CALENDAR_SOURCES}}', calendarArrayCode);
47
-
48
- // Write built HTML
49
- const outputPath = path.join(__dirname, 'index.html');
50
- fs.writeFileSync(outputPath, html, 'utf-8');
51
-
52
- console.log(`āœ“ Built index.html with ${calendarSources.length} calendar source(s)`);
53
-