From 258bacb7b042aa3812593e6b2b256a5261362562 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 20 May 2025 17:44:23 +0300 Subject: [PATCH] feat(ui): enhance calendar merging interface with search functionality --- public/index.html | 24 +++- public/script.js | 290 +++++++++++++++++++++++++++++++++++----------- public/styles.css | 30 ++++- src/routes.js | 61 ++++++++++ src/server.js | 18 ++- 5 files changed, 350 insertions(+), 73 deletions(-) diff --git a/public/index.html b/public/index.html index 5eeeebd..d876221 100644 --- a/public/index.html +++ b/public/index.html @@ -9,9 +9,11 @@

📅 Calendar Merger

- + +
+

Merge Calendars

-
+ +
+

After merging, your calendar URL will appear here.

+
+
+ + + +
+

Find Existing Calendar

+
+
+ + +
+
+
+
+
diff --git a/public/script.js b/public/script.js index bbaf0c7..58b1af9 100644 --- a/public/script.js +++ b/public/script.js @@ -2,7 +2,18 @@ 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 = ''; @@ -12,78 +23,221 @@ function isValidUrl(url) { return urlPattern.test(url); } - addCalendarButton.addEventListener('click', () => { - const newCalendar = document.createElement('div'); - newCalendar.className = 'calendar-entry'; - newCalendar.innerHTML = ` - - -
- - -
- - `; - calendars.appendChild(newCalendar); - calendarIndex++; - }); - - form.addEventListener('submit', (event) => { - event.preventDefault(); - const linkGroupName = document.getElementById('link-group-name').value; - const calendarsData = []; - let valid = true; // Flag to track URL validity +// 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); + } - 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 - }); - } + // 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; } - 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; }); + } 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(); + } + }); + } +} + +// Function to search for a calendar +function searchCalendar() { + let calendarName = calendarSearch.value.trim(); + if (!calendarName) { + searchResult.innerHTML = '
Please enter a calendar name
'; + return; + } + + // Extract just the calendar name if a URL was entered + calendarName = extractCalendarName(calendarName); + + searchResult.innerHTML = '
Loading...
'; + + // Check if calendar exists + fetch(`/calendar-config/${calendarName}`) + .then(response => response.json()) + .then(data => { + if (data.exists) { + searchResult.innerHTML = `
Calendar found!
`; + loadCalendarConfig(data.config, calendarName); + } else { + searchResult.innerHTML = `
Calendar not found
`; + } + }) + .catch(error => { + console.error('Error searching for calendar:', error); + searchResult.innerHTML = `
Error searching for calendar
`; + }); +} + +// 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 = ` + + +
+ + +
+ + `; + 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 = ` + + +
+ + +
+ + `; + 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'; + } + + if (isEditing) { + result.innerHTML = `Updated calendar URL: ${data.url}`; + + // Update the search result + if (searchResult) { + searchResult.innerHTML = `
Calendar updated successfully!
`; } - return response.json(); - }) - .then((data) => { - mergedUrl = data.url; + + // Reset editing state + isEditing = false; + currentCalendarName = ''; + } else { result.innerHTML = `Merged calendar URL: ${data.url}`; - 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(); } + + 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(); } - }); + } +}); \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index cc5168a..80d6637 100644 --- a/public/styles.css +++ b/public/styles.css @@ -112,6 +112,7 @@ background: var(--surface); border-radius: 8px; margin-top: 1rem; + margin-bottom: 2rem; border: 2px dashed var(--border); } @@ -156,4 +157,31 @@ content: '×'; font-size: 1.4rem; line-height: 1; - } \ No newline at end of file + } + + /* 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; +} \ No newline at end of file diff --git a/src/routes.js b/src/routes.js index d9c26c2..558ec6b 100644 --- a/src/routes.js +++ b/src/routes.js @@ -39,6 +39,67 @@ 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`); diff --git a/src/server.js b/src/server.js index b86cca0..49746dc 100644 --- a/src/server.js +++ b/src/server.js @@ -7,8 +7,22 @@ console.log(`Starting server in ${process.cwd()}`); const app = express(); app.use(express.json()); -// Serve static files from the 'public' directory -app.use(express.static(path.join(process.cwd(), 'public'))); +// Mount API routes BEFORE static files +// This ensures that API requests are handled by your routes first 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; \ No newline at end of file