Compare commits
No commits in common. "master" and "master" have entirely different histories.
12 changed files with 167 additions and 434 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
|||
node_modules/
|
||||
test/calendar/
|
||||
calendar/
|
||||
logs/
|
||||
.qodo
|
||||
logs/
|
BIN
.qodo/history.sqlite
Normal file
BIN
.qodo/history.sqlite
Normal file
Binary file not shown.
|
@ -7,9 +7,8 @@ The Calendar Merger project is a web application that allows users to merge mult
|
|||
- Add calendars
|
||||
- Specify prefixes for each calendar
|
||||
- Override event summaries if desired
|
||||
- Search for and edit previously merged calendars
|
||||
|
||||
The application also generates a unique URL for the merged calendar and updates it ever so often.
|
||||
The application also generates a unique URL for the merged calendar and updates it every hour using a cron job.
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -17,8 +16,7 @@ The application also generates a unique URL for the merged calendar and updates
|
|||
- Specify prefixes for each calendar
|
||||
- Optionally override event summaries
|
||||
- Generate a unique URL for the merged calendar
|
||||
- Automatically update the merged calendar
|
||||
- Search for and edit previously merged calendars
|
||||
- Automatically update the merged calendar every hour
|
||||
|
||||
## Calender Directory
|
||||
|
||||
|
|
|
@ -9,6 +9,6 @@ services:
|
|||
- NODE_ENV=production
|
||||
- NODE_PORT=3012
|
||||
volumes:
|
||||
- ./calendar:/Calmerge/calendar
|
||||
- ./logs:/Calmerge/logs
|
||||
- ./calendar:/app/calendar
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
# Set working directory inside the container
|
||||
WORKDIR /Calmerge
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package.json and package-lock.json for installing dependencies
|
||||
COPY package*.json ./
|
||||
|
|
|
@ -9,23 +9,9 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<h1>📅 Calendar Merger</h1>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="form-card search-section">
|
||||
<h2>Find Existing Calendar</h2>
|
||||
<div class="input-group">
|
||||
<div class="search-container">
|
||||
<input type="text" id="calendar-search" placeholder="Enter calendar name">
|
||||
<button type="button" id="search-btn" class="button primary-btn">Search</button>
|
||||
</div>
|
||||
<div id="search-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Merge Form -->
|
||||
|
||||
<div class="form-card">
|
||||
<form id="merge-form">
|
||||
<h2>Merge Calendars</h2>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
id="link-group-name"
|
||||
|
@ -55,10 +41,8 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Result Section -->
|
||||
<div id="result"></div>
|
||||
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
|
292
public/script.js
292
public/script.js
|
@ -2,18 +2,7 @@ const form = document.getElementById('merge-form');
|
|||
const calendars = document.getElementById('calendars');
|
||||
const addCalendarButton = document.getElementById('add-calendar');
|
||||
const result = document.getElementById('result');
|
||||
const resultInfo = document.querySelector('.result-info');
|
||||
|
||||
// Search functionality elements
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const calendarSearch = document.getElementById('calendar-search');
|
||||
const searchResult = document.getElementById('search-result');
|
||||
|
||||
// Variable to track if we're editing an existing calendar
|
||||
let isEditing = false;
|
||||
let currentCalendarName = '';
|
||||
|
||||
// Variable to track the index of the calendar being added
|
||||
let calendarIndex = 1;
|
||||
let mergedUrl = '';
|
||||
|
||||
|
@ -23,221 +12,78 @@ function isValidUrl(url) {
|
|||
return urlPattern.test(url);
|
||||
}
|
||||
|
||||
// Function to extract calendar name from URL or input
|
||||
function extractCalendarName(input) {
|
||||
// If it's a URL, extract the last part of the path
|
||||
if (input.startsWith('http')) {
|
||||
try {
|
||||
// Remove trailing slash if present
|
||||
if (input.endsWith('/')) {
|
||||
input = input.slice(0, -1);
|
||||
}
|
||||
|
||||
// Extract the last part of the path
|
||||
const url = new URL(input);
|
||||
const pathParts = url.pathname.split('/').filter(part => part.length > 0);
|
||||
|
||||
// If there's a path part, use the last one
|
||||
if (pathParts.length > 0) {
|
||||
let lastPart = pathParts[pathParts.length - 1];
|
||||
|
||||
// Remove .ics extension if present
|
||||
if (lastPart.endsWith('.ics')) {
|
||||
lastPart = lastPart.slice(0, -4);
|
||||
}
|
||||
|
||||
return lastPart;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing URL:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// If not a URL or URL parsing failed, just return the input as is
|
||||
return input;
|
||||
}
|
||||
|
||||
// Event listener for the search button
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', searchCalendar);
|
||||
|
||||
// Also search when pressing Enter in the search field
|
||||
if (calendarSearch) {
|
||||
calendarSearch.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchCalendar();
|
||||
}
|
||||
addCalendarButton.addEventListener('click', () => {
|
||||
const newCalendar = document.createElement('div');
|
||||
newCalendar.className = 'calendar-entry';
|
||||
newCalendar.innerHTML = `
|
||||
<input type="url" id="url-${calendarIndex}" placeholder="https://example.com/calendar.ics">
|
||||
<input type="text" id="prefix-${calendarIndex}" placeholder="Event prefix">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="override-${calendarIndex}">
|
||||
<label for="override-${calendarIndex}">Override</label>
|
||||
</div>
|
||||
<button type="button" class="remove-btn" title="Remove calendar"></button>
|
||||
`;
|
||||
calendars.appendChild(newCalendar);
|
||||
calendarIndex++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to search for a calendar
|
||||
function searchCalendar() {
|
||||
let calendarName = calendarSearch.value.trim();
|
||||
if (!calendarName) {
|
||||
searchResult.innerHTML = '<div class="alert alert-warning">Please enter a calendar name</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract just the calendar name if a URL was entered
|
||||
calendarName = extractCalendarName(calendarName);
|
||||
|
||||
searchResult.innerHTML = '<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
|
||||
// Check if calendar exists
|
||||
fetch(`/calendar-config/${calendarName}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.exists) {
|
||||
searchResult.innerHTML = `<div class="alert alert-success">Calendar found!</div>`;
|
||||
loadCalendarConfig(data.config, calendarName);
|
||||
} else {
|
||||
searchResult.innerHTML = `<div class="alert alert-danger">Calendar not found</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error searching for calendar:', error);
|
||||
searchResult.innerHTML = `<div class="alert alert-danger">Error searching for calendar</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to load calendar configuration for editing
|
||||
function loadCalendarConfig(config, calendarName) {
|
||||
// Set editing mode
|
||||
isEditing = true;
|
||||
currentCalendarName = calendarName;
|
||||
|
||||
// Update link group name
|
||||
document.getElementById('link-group-name').value = config.linkGroupName;
|
||||
|
||||
// Clear existing calendars
|
||||
while (calendars.children.length > 0) {
|
||||
calendars.removeChild(calendars.lastChild);
|
||||
}
|
||||
|
||||
// Reset calendar index
|
||||
calendarIndex = 0;
|
||||
|
||||
// Add calendars from config
|
||||
config.calendars.forEach(calendar => {
|
||||
const newCalendar = document.createElement('div');
|
||||
newCalendar.className = 'calendar-entry';
|
||||
newCalendar.innerHTML = `
|
||||
<input type="url" id="url-${calendarIndex}" placeholder="https://example.com/calendar.ics" value="${calendar.url}">
|
||||
<input type="text" id="prefix-${calendarIndex}" placeholder="Event prefix" value="${calendar.prefix || ''}">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="override-${calendarIndex}" ${calendar.override ? 'checked' : ''}>
|
||||
<label for="override-${calendarIndex}">Override</label>
|
||||
</div>
|
||||
<button type="button" class="remove-btn" title="Remove calendar"></button>
|
||||
`;
|
||||
calendars.appendChild(newCalendar);
|
||||
calendarIndex++;
|
||||
});
|
||||
|
||||
// Scroll to the form
|
||||
form.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Event listener for adding new calendar
|
||||
addCalendarButton.addEventListener('click', () => {
|
||||
const newCalendar = document.createElement('div');
|
||||
newCalendar.className = 'calendar-entry';
|
||||
newCalendar.innerHTML = `
|
||||
<input type="url" id="url-${calendarIndex}" placeholder="https://example.com/calendar.ics">
|
||||
<input type="text" id="prefix-${calendarIndex}" placeholder="Event prefix">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="override-${calendarIndex}">
|
||||
<label for="override-${calendarIndex}">Override</label>
|
||||
</div>
|
||||
<button type="button" class="remove-btn" title="Remove calendar"></button>
|
||||
`;
|
||||
calendars.appendChild(newCalendar);
|
||||
calendarIndex++;
|
||||
});
|
||||
|
||||
// Event listener for form submission
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const linkGroupName = document.getElementById('link-group-name').value;
|
||||
const calendarsData = [];
|
||||
let valid = true; // Flag to track URL validity
|
||||
|
||||
for (let i = 0; i < calendarIndex; i++) {
|
||||
const prefix = document.getElementById(`prefix-${i}`);
|
||||
const override = document.getElementById(`override-${i}`);
|
||||
const url = document.getElementById(`url-${i}`);
|
||||
if (prefix && override && url && url.value) {
|
||||
// Validate the URL
|
||||
if (url.value.startsWith('http') && !isValidUrl(url.value)) {
|
||||
valid = false; // Set flag to false if any URL is invalid
|
||||
alert(`Invalid URL format for calendar ${i + 1}: ${url.value}`);
|
||||
} else {
|
||||
calendarsData.push({
|
||||
prefix: prefix.value,
|
||||
override: override.checked,
|
||||
url: url.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
// Determine if we're creating or updating
|
||||
const endpoint = isEditing ? `/calendar/${currentCalendarName}` : '/merge';
|
||||
const method = isEditing ? 'PUT' : 'POST';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ linkGroupName, calendars: calendarsData })
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw err; });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
mergedUrl = data.url;
|
||||
|
||||
// Hide the info text when showing results
|
||||
if (resultInfo) {
|
||||
resultInfo.style.display = 'none';
|
||||
}
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const linkGroupName = document.getElementById('link-group-name').value;
|
||||
const calendarsData = [];
|
||||
let valid = true; // Flag to track URL validity
|
||||
|
||||
if (isEditing) {
|
||||
result.innerHTML = `Updated calendar URL: <a href="${data.url}">${data.url}</a>`;
|
||||
|
||||
// Update the search result
|
||||
if (searchResult) {
|
||||
searchResult.innerHTML = `<div class="alert alert-success">Calendar updated successfully!</div>`;
|
||||
for (let i = 0; i < calendarIndex; i++) {
|
||||
const prefix = document.getElementById(`prefix-${i}`);
|
||||
const override = document.getElementById(`override-${i}`);
|
||||
const url = document.getElementById(`url-${i}`);
|
||||
|
||||
if (prefix && override && url) {
|
||||
// Validate the URL
|
||||
if (!isValidUrl(url.value)) {
|
||||
valid = false; // Set flag to false if any URL is invalid
|
||||
alert(`Invalid URL format for calendar ${i + 1}: ${url.value}`);
|
||||
} else {
|
||||
calendarsData.push({
|
||||
prefix: prefix.value,
|
||||
override: override.checked,
|
||||
url: url.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset editing state
|
||||
isEditing = false;
|
||||
currentCalendarName = '';
|
||||
} else {
|
||||
}
|
||||
if (valid) {
|
||||
fetch('/merge', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ linkGroupName, calendars: calendarsData })
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw err; });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
mergedUrl = data.url;
|
||||
result.innerHTML = `Merged calendar URL: <a href="${data.url}">${data.url}</a>`;
|
||||
}
|
||||
|
||||
console.log('Operation completed successfully!');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
result.innerHTML = `Error: ${error.message || 'Unknown error'}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for removing a calendar entry
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('remove-btn')) {
|
||||
const calendarEntry = event.target.closest('.calendar-entry');
|
||||
if (calendarEntry) {
|
||||
calendarEntry.remove();
|
||||
console.log('Links added successfully!');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
result.innerHTML = `Error merging calendars: ${error.message || 'Unknown error'}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('remove-btn')) {
|
||||
const calendarEntry = event.target.closest('.calendar-entry');
|
||||
if (calendarEntry) {
|
||||
calendarEntry.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -112,7 +112,6 @@
|
|||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 2px dashed var(--border);
|
||||
}
|
||||
|
||||
|
@ -157,31 +156,4 @@
|
|||
content: '×';
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Search Section Styling */
|
||||
.search-section {
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-container button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#search-result {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
|
@ -15,18 +15,33 @@ export const sanitizeFilename = (filename) => filename.replace(/[<>:"/\\|?* ]/g,
|
|||
// Fetch calendar data from URL or file
|
||||
export async function fetchCalendarData(calendar) {
|
||||
const isFilePath = !calendar.url.startsWith('http');
|
||||
if (isFilePath) {
|
||||
// logger.debug(`Reading calendar from file: ${calendar.url}`);
|
||||
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar };
|
||||
}
|
||||
try {
|
||||
if (isFilePath) {
|
||||
// logger.debug(`Reading calendar from file: ${calendar.url}`);
|
||||
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar };
|
||||
} else {
|
||||
// logger.debug(`Fetching calendar from URL: ${calendar.url}`);
|
||||
const response = await axios.get(calendar.url);
|
||||
return { data: response.data, ...calendar };
|
||||
// First try the original URL
|
||||
const initialResponse = await axios.get(calendar.url);
|
||||
return { data: initialResponse.data, ...calendar };
|
||||
} catch (initialError) {
|
||||
logger.debug(`Initial fetch failed, trying extension adjustment for: ${calendar.url}`);
|
||||
|
||||
// Determine alternate URL version
|
||||
const altUrl = calendar.url.endsWith('.ics')
|
||||
? calendar.url.slice(0, -4) // Remove .ics
|
||||
: calendar.url + '.ics'; // Add .ics
|
||||
|
||||
try {
|
||||
// Try the alternate version
|
||||
const altResponse = await axios.get(altUrl);
|
||||
logger.debug(`Success with adjusted URL: ${altUrl}`);
|
||||
return { data: altResponse.data, ...calendar };
|
||||
} catch (altError) {
|
||||
logger.error(`Both URL versions failed:
|
||||
Original: ${calendar.url}
|
||||
Adjusted: ${altUrl}`);
|
||||
throw new Error(`Calendar fetch failed for both URL versions`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
|
||||
throw new Error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,67 +39,6 @@ router.post('/merge', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// New endpoint to check if a calendar exists and return its configuration
|
||||
router.get('/calendar-config/:name', (req, res) => {
|
||||
const calendarName = sanitizeFilename(req.params.name);
|
||||
const jsonFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.json`);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(jsonFilePath)) {
|
||||
const configData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'));
|
||||
res.json({
|
||||
exists: true,
|
||||
config: configData,
|
||||
url: `${req.protocol}://${req.get('host')}/calendar/${calendarName}`
|
||||
});
|
||||
} else {
|
||||
res.json({ exists: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking calendar:', error.message);
|
||||
res.status(500).json({ error: 'Failed to check calendar configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update an existing calendar configuration
|
||||
router.put('/calendar/:name', async (req, res) => {
|
||||
const calendarName = sanitizeFilename(req.params.name);
|
||||
const { linkGroupName, calendars } = req.body;
|
||||
|
||||
// Validate the input
|
||||
if (!linkGroupName || !Array.isArray(calendars) || calendars.length === 0) {
|
||||
return res.status(400).json({ error: 'Invalid input. Please provide a linkGroupName and at least one calendar.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the calendar exists
|
||||
const jsonFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.json`);
|
||||
if (!fs.existsSync(jsonFilePath)) {
|
||||
return res.status(404).json({ error: 'Calendar not found.' });
|
||||
}
|
||||
|
||||
// Fetch calendar data
|
||||
const results = await Promise.all(calendars.map(fetchCalendarData));
|
||||
|
||||
// Generate merged calendar using ical.js
|
||||
const calendarComponent = createCalendarComponent(linkGroupName);
|
||||
addEventsToCalendar(calendarComponent, results);
|
||||
|
||||
// Save the updated calendar to a file
|
||||
saveCalendarFile(`${calendarName}.ics`, calendarComponent.toString());
|
||||
|
||||
// Save the updated configuration
|
||||
saveCalendarFile(`${calendarName}.json`, JSON.stringify({ linkGroupName, calendars }, null, 2));
|
||||
|
||||
res.json({
|
||||
url: `${req.protocol}://${req.get('host')}/calendar/${calendarName}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating calendar:', error.message);
|
||||
res.status(500).json({ error: 'Failed to update calendar' });
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh calendar if outdated
|
||||
async function refreshCalendarData(calendarName) {
|
||||
const jsonFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.json`);
|
||||
|
@ -118,15 +57,9 @@ async function refreshCalendarData(calendarName) {
|
|||
|
||||
// Serve the merged calendar file and refresh if older than an hour
|
||||
router.get('/calendar/:name', async (req, res) => {
|
||||
// Extract the calendar name and remove .ics extension if present
|
||||
let calendarName = req.params.name;
|
||||
if (calendarName.endsWith('.ics')) {
|
||||
calendarName = calendarName.slice(0, -4);
|
||||
}
|
||||
|
||||
const calendarName = req.params.name;
|
||||
const icsFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.ics`);
|
||||
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
||||
|
||||
|
||||
try {
|
||||
// Check if the .ics file exists
|
||||
console.log(`Serving calendar for: ${calendarName}`);
|
||||
|
@ -140,11 +73,7 @@ router.get('/calendar/:name', async (req, res) => {
|
|||
}
|
||||
res.setHeader('Content-Type', 'text/calendar');
|
||||
res.sendFile(icsFilePath);
|
||||
|
||||
// Log the successful request with URL format and status code
|
||||
console.log(`Serving calendar ${calendarName} for ${fullUrl}: 200`);
|
||||
} else {
|
||||
console.log(`Calendar not found: ${calendarName} for ${fullUrl}: 404`);
|
||||
res.status(404).json({ error: 'Calendar not found.' });
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -153,4 +82,4 @@ router.get('/calendar/:name', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
|
|
@ -7,22 +7,8 @@ console.log(`Starting server in ${process.cwd()}`);
|
|||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Mount API routes BEFORE static files
|
||||
// This ensures that API requests are handled by your routes first
|
||||
// Serve static files from the 'public' directory
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
app.use('/', routes);
|
||||
|
||||
// Serve static files from the 'public' directory
|
||||
// This will only handle requests that weren't matched by the routes
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
// Optional: Add a catch-all route for client-side routing
|
||||
app.get('*', (req, res) => {
|
||||
// Only serve index.html for non-API, non-file requests
|
||||
if (!req.path.startsWith('/api') && !req.path.includes('.')) {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
|
||||
} else {
|
||||
res.status(404).send('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -2,12 +2,15 @@ import request from 'supertest';
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { fetchCalendarData } from '../src/calendarUtil.js';
|
||||
|
||||
// ESM equivalent of __dirname
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CALENDARS_DIR = path.join(__dirname, 'calendar');
|
||||
const CALENDARS_DIR = path.join(process.cwd(), 'calendar');
|
||||
const TEST_CALENDARS_DIR = path.join(__dirname, 'test_calendars');
|
||||
const EXPECTED_OUTPUTS_DIR = path.join(__dirname, 'expected_outputs');
|
||||
|
||||
|
@ -26,9 +29,8 @@ describe('Calendar Merging API', () => {
|
|||
await new Promise(resolve => server.close(resolve));
|
||||
|
||||
// Clean up the merged calendars directory after tests
|
||||
fs.rmdirSync(CALENDARS_DIR, { recursive: true });
|
||||
if (fs.existsSync(CALENDARS_DIR)) {
|
||||
fs.rmdirSync(CALENDARS_DIR, { recursive: true });
|
||||
fs.rmSync(CALENDARS_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Optional: Add a delay to ensure all handles are released
|
||||
|
@ -280,58 +282,60 @@ describe('Calendar Merging API', () => {
|
|||
expect(actualOutput).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
// Test accessing calendar with and without .ics extension
|
||||
test('Access calendar with and without .ics extension', async () => {
|
||||
// Create a test calendar
|
||||
const response = await request(server)
|
||||
.post('/merge')
|
||||
.send({
|
||||
linkGroupName: 'Extension Test Calendar',
|
||||
calendars: [
|
||||
{
|
||||
url: getTestCalendarFilename('sf_public_holidays.ics'),
|
||||
prefix: 'Test',
|
||||
override: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
describe('Smart URL Handling', () => {
|
||||
let mockAxios;
|
||||
|
||||
beforeAll(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Check if the file was created
|
||||
const filePath = path.join(CALENDARS_DIR, 'Extension_Test_Calendar.ics');
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
// Test accessing without .ics extension
|
||||
const responseWithoutExtension = await request(server)
|
||||
.get('/calendar/Extension_Test_Calendar');
|
||||
|
||||
expect(responseWithoutExtension.status).toBe(200);
|
||||
expect(responseWithoutExtension.headers['content-type']).toMatch(/text\/calendar/);
|
||||
|
||||
// Test accessing with .ics extension
|
||||
const responseWithExtension = await request(server)
|
||||
.get('/calendar/Extension_Test_Calendar.ics');
|
||||
|
||||
expect(responseWithExtension.status).toBe(200);
|
||||
expect(responseWithExtension.headers['content-type']).toMatch(/text\/calendar/);
|
||||
|
||||
// Verify both responses contain the same content
|
||||
expect(responseWithoutExtension.text).toBe(responseWithExtension.text);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
});
|
||||
|
||||
// Test 404 response for non-existent calendar
|
||||
test('Return 404 for non-existent calendar with and without .ics extension', async () => {
|
||||
// Test accessing non-existent calendar without .ics extension
|
||||
const responseWithoutExtension = await request(server)
|
||||
.get('/calendar/NonExistentCalendar');
|
||||
|
||||
expect(responseWithoutExtension.status).toBe(404);
|
||||
|
||||
// Test accessing non-existent calendar with .ics extension
|
||||
const responseWithExtension = await request(server)
|
||||
.get('/calendar/NonExistentCalendar.ics');
|
||||
|
||||
expect(responseWithExtension.status).toBe(404);
|
||||
afterAll(() => {
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
test('should use original URL when valid without .ics', async () => {
|
||||
const validUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder';
|
||||
mockAxios.onGet(validUrl).reply(200, 'VALID_CALENDAR');
|
||||
|
||||
const result = await fetchCalendarData({ url: validUrl });
|
||||
expect(result.data).toBe('VALID_CALENDAR');
|
||||
});
|
||||
|
||||
test('should try .ics version when original fails', async () => {
|
||||
const invalidUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder.ics';
|
||||
const validUrl = invalidUrl.slice(0, -4);
|
||||
|
||||
mockAxios
|
||||
.onGet(invalidUrl).reply(404)
|
||||
.onGet(validUrl).reply(200, 'VALID_CALENDAR');
|
||||
|
||||
const result = await fetchCalendarData({ url: invalidUrl });
|
||||
expect(result.data).toBe('VALID_CALENDAR');
|
||||
});
|
||||
|
||||
test('should preserve valid .ics URLs', async () => {
|
||||
const googleUrl = 'https://calendar.google.com/.../basic.ics';
|
||||
mockAxios.onGet(googleUrl).reply(200, 'GOOGLE_CALENDAR');
|
||||
|
||||
const result = await fetchCalendarData({ url: googleUrl });
|
||||
expect(result.data).toBe('GOOGLE_CALENDAR');
|
||||
});
|
||||
|
||||
test('should try both versions for ambiguous URLs', async () => {
|
||||
const baseUrl = 'https://example.com/calendar';
|
||||
const icsUrl = baseUrl + '.ics';
|
||||
|
||||
mockAxios
|
||||
.onGet(baseUrl).reply(404)
|
||||
.onGet(icsUrl).reply(200, 'ICS_CALENDAR');
|
||||
|
||||
const result = await fetchCalendarData({ url: baseUrl });
|
||||
expect(result.data).toBe('ICS_CALENDAR');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue