diff --git a/app.js b/app.js index 47aaeed..64685b3 100644 --- a/app.js +++ b/app.js @@ -895,6 +895,7 @@ app.post("/api/search", async (req, res) => { } }); + // Improved logging endpoint app.post('/api/log', (req, res) => { const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress; diff --git a/src/index.html b/src/index.html index 11ef4cb..9a4d62a 100644 --- a/src/index.html +++ b/src/index.html @@ -32,6 +32,7 @@ + diff --git a/src/js/backend.js b/src/js/backend.js index a0aa20a..d252000 100644 --- a/src/js/backend.js +++ b/src/js/backend.js @@ -1,184 +1,20 @@ -let tutorialDismissed = false; -let currentData = null; -let outputFormat = "json"; +// Export the functions that frontend.js needs to call +window.backendAPI = { + fetchData, + jsonToYAML, + formatDuration, + formatEpochTime, + processTimestamps +}; + +window.appState = { + currentData: null, + outputFormat: "json", + tutorialDismissed: false +}; -// Initialize once DOM is loaded document.addEventListener("DOMContentLoaded", function() { - initTabSwitching(); - addEnterKeyListeners(); - setupDownloadButton(); - setupFormatSelector(); - setupProcessingOptions(); - setupTimeOptions(); - addSyncListeners(); -}); - -// Tab switching logic -function initTabSwitching() { - document.querySelectorAll(".tab").forEach((tab) => { - tab.addEventListener("click", () => { - document - .querySelectorAll(".tab") - .forEach((t) => t.classList.remove("active")); - document - .querySelectorAll(".tab-content") - .forEach((c) => c.classList.remove("active")); - - tab.classList.add("active"); - const tabId = tab.getAttribute("data-tab"); - document.getElementById(`${tabId}-tab`).classList.add("active"); - }); - }); -} - -// Setup processing options (sanitize/replace) -function setupProcessingOptions() { - document.getElementById("sanitizeOption").addEventListener("change", function() { - if (currentData) { - // Re-fetch with new options - const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); - triggerActiveTabButton(); - } - }); - - document.getElementById("replaceKeysOption").addEventListener("change", function() { - if (currentData) { - // Re-fetch with new options - const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); - triggerActiveTabButton(); - } - }); -} - -// Setup format selector -function setupFormatSelector() { - document.getElementById("outputFormat").addEventListener("change", function() { - outputFormat = this.value; - if (currentData) { - displayResults(currentData); - } - }); -} - -// Fetch stats -document.getElementById("fetchStats").addEventListener("click", async () => { - const username = document.getElementById("username").value.trim(); - const ssoToken = document.getElementById("ssoToken").value.trim(); - const platform = document.getElementById("platform").value; - const game = document.getElementById("game").value; - const apiCall = document.getElementById("apiCall").value; - - const sanitize = document.getElementById("sanitizeOption").checked; - const replaceKeys = document.getElementById("replaceKeysOption").checked; - - await fetchData("/api/stats", { - username, - ssoToken, - platform, - game, - apiCall, - sanitize, - replaceKeys - }); -}); - -// Fetch match history -document.getElementById("fetchMatches").addEventListener("click", async () => { - const username = document.getElementById("matchUsername").value.trim(); - const ssoToken = document.getElementById("ssoToken").value.trim(); - const platform = document.getElementById("matchPlatform").value; - const game = document.getElementById("matchGame").value; - - const sanitize = document.getElementById("sanitizeOption").checked; - const replaceKeys = document.getElementById("replaceKeysOption").checked; - - await fetchData("/api/matches", { - username, - ssoToken, - platform, - game, - sanitize, - replaceKeys - }); -}); - -// Fetch match details -document.getElementById("fetchMatchInfo").addEventListener("click", async () => { - const matchId = document.getElementById("matchId").value.trim(); - const ssoToken = document.getElementById("ssoToken").value.trim(); - const platform = document.getElementById("matchPlatform").value; - const game = document.getElementById("matchGame").value; - - const sanitize = document.getElementById("sanitizeOption").checked; - const replaceKeys = document.getElementById("replaceKeysOption").checked; - - if (!matchId) { - displayError("Match ID is required"); - return; - } - - await fetchData("/api/matchInfo", { - matchId, - ssoToken, - platform, - game, - sanitize, - replaceKeys - }); -}); - -// Fetch user info -document.getElementById("fetchUserInfo").addEventListener("click", async () => { - const username = document.getElementById("userUsername").value.trim(); - const ssoToken = document.getElementById("ssoToken").value.trim(); - const platform = document.getElementById("userPlatform").value; - const userCall = document.getElementById("userCall").value; - - const sanitize = document.getElementById("sanitizeOption").checked; - const replaceKeys = document.getElementById("replaceKeysOption").checked; - - // For event feed and identities, username is not required - if ( - !username && - userCall !== "eventFeed" && - userCall !== "friendFeed" && - userCall !== "identities" - ) { - displayError("Username is required for this API call"); - return; - } - - await fetchData("/api/user", { - username, - ssoToken, - platform, - userCall, - sanitize, - replaceKeys - }); -}); - -// Fuzzy search -document.getElementById("fuzzySearch").addEventListener("click", async () => { - const username = document.getElementById("searchUsername").value.trim(); - const ssoToken = document.getElementById("ssoToken").value.trim(); - const platform = document.getElementById("searchPlatform").value; - - const sanitize = document.getElementById("sanitizeOption").checked; - const replaceKeys = document.getElementById("replaceKeysOption").checked; - - if (!username) { - displayError("Username is required for search"); - return; - } - - await fetchData("/api/search", { - username, - ssoToken, - platform, - sanitize, - replaceKeys - }); + // Backend-specific initialization }); // YAML conversion function @@ -251,8 +87,8 @@ async function fetchData(endpoint, requestData) { loadingElement.style.display = "block"; // Hide tutorial if not already dismissed - if (!tutorialDismissed) { - tutorialDismissed = true; + if (!window.appState.tutorialDismissed) { + window.appState.tutorialDismissed = true; document.querySelectorAll(".tutorial").forEach(element => { element.style.display = "none"; }); @@ -260,7 +96,7 @@ async function fetchData(endpoint, requestData) { // Validate request data if (!requestData.ssoToken) { - displayError("SSO Token is required"); + window.uiAPI.displayError("SSO Token is required"); loadingElement.style.display = "none"; return; } @@ -297,18 +133,18 @@ async function fetchData(endpoint, requestData) { } if (data.error) { - displayError(data.error); + window.uiAPI.displayError(data.error); } else if (data.status === "error") { - displayError(data.message || "An error occurred"); + window.uiAPI.displayError(data.message || "An error occurred"); } else { - currentData = data; - displayResults(data); + window.appState.currentData = data; + window.uiAPI.displayResults(data); } } catch (error) { if (error.name === 'AbortError') { - displayError("Request timed out. Please try again."); + window.uiAPI.displayError("Request timed out. Please try again."); } else { - displayError( + window.uiAPI.displayError( `Error: ${error.message || "An error occurred while fetching data."}` ); console.error("Fetch error:", error); @@ -318,108 +154,6 @@ async function fetchData(endpoint, requestData) { } } -// Function to handle time and duration conversion -function displayResults(data) { - const resultsElement = document.getElementById("results"); - const downloadContainer = document.getElementById("download-container"); - - // Apply time conversion if enabled - const convertTime = document.getElementById('convertTimeOption').checked; - const replaceKeys = document.getElementById('replaceKeysOption').checked; - let displayData = data; - - if (convertTime || replaceKeys) { - const timezone = document.getElementById('timezoneSelect').value; - displayData = processTimestamps(structuredClone(data), timezone); // Use structured clone API instead of JSON.parse/stringify - // displayData = processTimestamps(JSON.parse(JSON.stringify(data)), timezone); - } - - // Format the data - let formattedData = ''; - if (outputFormat === 'yaml') { - formattedData = jsonToYAML(displayData); - document.getElementById("downloadJson").textContent = "Download YAML Data"; - } else { - formattedData = JSON.stringify(displayData, null, 2); - document.getElementById("downloadJson").textContent = "Download JSON Data"; - } - - resultsElement.textContent = formattedData; - resultsElement.style.display = "block"; - downloadContainer.style.display = "block"; -} - -// Helper function to display errors -function displayError(message) { - const errorElement = document.getElementById("error"); - const loadingElement = document.getElementById("loading"); - const resultsElement = document.getElementById("results"); - - errorElement.textContent = message; - loadingElement.style.display = "none"; - - // Clear previous results to ensure they can be redrawn - resultsElement.style.display = "none"; - resultsElement.textContent = ""; - - // Keep tutorial hidden if previously dismissed - if (tutorialDismissed) { - document.querySelectorAll(".tutorial").forEach(element => { - element.style.display = "none"; - }); - } -} - -function addEnterKeyListeners() { - // Use event delegation for handling Enter key press - document.addEventListener("keypress", function(event) { - if (event.key === "Enter") { - // Get the active element - const activeElement = document.activeElement; - - if (!activeElement || !activeElement.id) return; - - // Mapping of input fields to their submit buttons - const inputToButtonMapping = { - "ssoToken": null, // Will trigger active tab button - "username": null, // Will trigger active tab button - "matchUsername": "fetchMatches", - "matchId": "fetchMatchInfo", - "userUsername": "fetchUserInfo", - "searchUsername": "fuzzySearch" - }; - - if (activeElement.id in inputToButtonMapping) { - if (inputToButtonMapping[activeElement.id]) { - // Click the specific button - document.getElementById(inputToButtonMapping[activeElement.id]).click(); - } else { - // Trigger the active tab button - triggerActiveTabButton(); - } - } - } - }); -} - -function triggerActiveTabButton() { - const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); - switch (activeTab) { - case "stats": - document.getElementById("fetchStats").click(); - break; - case "matches": - document.getElementById("fetchMatches").click(); - break; - case "user": - document.getElementById("fetchUserInfo").click(); - break; - case "other": - document.getElementById("fuzzySearch").click(); - break; - } -} - // Function to convert seconds to human readable duration function formatDuration(seconds) { if (!seconds || isNaN(seconds)) return seconds; @@ -490,101 +224,6 @@ function processTimestamps(data, timezone, keysToConvert = ['date', 'dateAdded', return result; } -// Time options -function setupTimeOptions() { - const convertTimeCheckbox = document.getElementById('convertTimeOption'); - const timezoneSelect = document.getElementById('timezoneSelect'); - - convertTimeCheckbox.addEventListener('change', function() { - timezoneSelect.disabled = !this.checked; - - if (currentData) { - displayResults(currentData); // Refresh the display - } - }); - - timezoneSelect.addEventListener('change', function() { - if (currentData) { - displayResults(currentData); // Refresh the display - } - }); -} - -// Download Button -function setupDownloadButton() { - const downloadBtn = document.getElementById("downloadJson"); - if (!downloadBtn) return; - - downloadBtn.addEventListener("click", function() { - const resultsElement = document.getElementById("results"); - const jsonData = resultsElement.textContent; - - if (!jsonData) { - alert("No data to download"); - return; - } - - // Create a Blob with the data - const contentType = outputFormat === 'yaml' ? 'text/yaml' : 'application/json'; - const blob = new Blob([jsonData], { type: contentType }); - - // Create a temporary link element - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - - // Generate a filename with timestamp - const date = new Date(); - const timestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}`; - const extension = outputFormat === 'yaml' ? 'yaml' : 'json'; - a.download = `cod_stats_${timestamp}.${extension}`; - - // Trigger download - document.body.appendChild(a); - a.click(); - - // Clean up - document.body.removeChild(a); - }); -} -// Function to synchronize username across tabs -function syncUsernames() { - const mainUsername = document.getElementById("username").value.trim(); - - // Only sync if there's a value - if (mainUsername) { - document.getElementById("matchUsername").value = mainUsername; - document.getElementById("userUsername").value = mainUsername; - document.getElementById("searchUsername").value = mainUsername; - } - - // Also sync platform across tabs when it changes - const mainPlatform = document.getElementById("platform").value; - document.getElementById("matchPlatform").value = mainPlatform; - document.getElementById("userPlatform").value = mainPlatform; - document.getElementById("searchPlatform").value = mainPlatform; -} - -// Sync listeners for persistent usernames -function addSyncListeners() { - // Add change listeners for username sync - document.getElementById("username").addEventListener("change", syncUsernames); - document.getElementById("matchUsername").addEventListener("change", function() { - document.getElementById("username").value = this.value; - syncUsernames(); - }); - document.getElementById("userUsername").addEventListener("change", function() { - document.getElementById("username").value = this.value; - syncUsernames(); - }); - document.getElementById("searchUsername").addEventListener("change", function() { - document.getElementById("username").value = this.value; - syncUsernames(); - }); - - // Add change listeners for platform sync - document.getElementById("platform").addEventListener("change", syncUsernames); -} - // Initialize session tracking when the page loads document.addEventListener('DOMContentLoaded', () => { // Generate a unique session ID @@ -888,4 +527,4 @@ document.addEventListener('DOMContentLoaded', () => { stack: e.error?.stack }); }); -}); \ No newline at end of file +}); diff --git a/src/js/frontend.js b/src/js/frontend.js new file mode 100644 index 0000000..85707ed --- /dev/null +++ b/src/js/frontend.js @@ -0,0 +1,380 @@ +window.uiAPI = { + displayResults, + displayError + }; + +// Initialize once DOM is loaded +document.addEventListener("DOMContentLoaded", function() { + initTabSwitching(); + addEnterKeyListeners(); + setupDownloadButton(); + setupFormatSelector(); + setupProcessingOptions(); + setupTimeOptions(); + addSyncListeners(); +}); + +// Tab switching logic +function initTabSwitching() { + document.querySelectorAll(".tab").forEach((tab) => { + tab.addEventListener("click", () => { + document + .querySelectorAll(".tab") + .forEach((t) => t.classList.remove("active")); + document + .querySelectorAll(".tab-content") + .forEach((c) => c.classList.remove("active")); + + tab.classList.add("active"); + const tabId = tab.getAttribute("data-tab"); + document.getElementById(`${tabId}-tab`).classList.add("active"); + }); + }); +} + +// Setup processing options (sanitize/replace) +function setupProcessingOptions() { + document.getElementById("sanitizeOption").addEventListener("change", function() { + if (window.appState.currentData) { // Call window.appState + // Re-fetch with new options + const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); + triggerActiveTabButton(); + } + }); + + document.getElementById("replaceKeysOption").addEventListener("change", function() { + if (window.appState.currentData) { + // Re-fetch with new options + const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); + triggerActiveTabButton(); + } + }); +} + +// Setup format selector +function setupFormatSelector() { + document.getElementById("outputFormat").addEventListener("change", function() { + window.appState.outputFormat = this.value; + if (window.appState.currentData) { + displayResults(window.appState.currentData); + } + }); + } + + // Fetch stats + document.getElementById("fetchStats").addEventListener("click", async () => { + const username = document.getElementById("username").value.trim(); + const ssoToken = document.getElementById("ssoToken").value.trim(); + const platform = document.getElementById("platform").value; + const game = document.getElementById("game").value; + const apiCall = document.getElementById("apiCall").value; + + const sanitize = document.getElementById("sanitizeOption").checked; + const replaceKeys = document.getElementById("replaceKeysOption").checked; + + await window.backendAPI.fetchData("/api/stats", { + username, + ssoToken, + platform, + game, + apiCall, + sanitize, + replaceKeys + }); + }); + + // Fetch match history + document.getElementById("fetchMatches").addEventListener("click", async () => { + const username = document.getElementById("matchUsername").value.trim(); + const ssoToken = document.getElementById("ssoToken").value.trim(); + const platform = document.getElementById("matchPlatform").value; + const game = document.getElementById("matchGame").value; + + const sanitize = document.getElementById("sanitizeOption").checked; + const replaceKeys = document.getElementById("replaceKeysOption").checked; + + await window.backendAPI.fetchData("/api/matches", { + username, + ssoToken, + platform, + game, + sanitize, + replaceKeys + }); + }); + + // Fetch match details + document.getElementById("fetchMatchInfo").addEventListener("click", async () => { + const matchId = document.getElementById("matchId").value.trim(); + const ssoToken = document.getElementById("ssoToken").value.trim(); + const platform = document.getElementById("matchPlatform").value; + const game = document.getElementById("matchGame").value; + + const sanitize = document.getElementById("sanitizeOption").checked; + const replaceKeys = document.getElementById("replaceKeysOption").checked; + + if (!matchId) { + displayError("Match ID is required"); + return; + } + + await window.backendAPI.fetchData("/api/matchInfo", { + matchId, + ssoToken, + platform, + game, + sanitize, + replaceKeys + }); + }); + + // Fetch user info + document.getElementById("fetchUserInfo").addEventListener("click", async () => { + const username = document.getElementById("userUsername").value.trim(); + const ssoToken = document.getElementById("ssoToken").value.trim(); + const platform = document.getElementById("userPlatform").value; + const userCall = document.getElementById("userCall").value; + + const sanitize = document.getElementById("sanitizeOption").checked; + const replaceKeys = document.getElementById("replaceKeysOption").checked; + + // For event feed and identities, username is not required + if ( + !username && + userCall !== "eventFeed" && + userCall !== "friendFeed" && + userCall !== "identities" + ) { + displayError("Username is required for this API call"); + return; + } + + await window.backendAPI.fetchData("/api/user", { + username, + ssoToken, + platform, + userCall, + sanitize, + replaceKeys + }); + }); + + // Fuzzy search + document.getElementById("fuzzySearch").addEventListener("click", async () => { + const username = document.getElementById("searchUsername").value.trim(); + const ssoToken = document.getElementById("ssoToken").value.trim(); + const platform = document.getElementById("searchPlatform").value; + + const sanitize = document.getElementById("sanitizeOption").checked; + const replaceKeys = document.getElementById("replaceKeysOption").checked; + + if (!username) { + displayError("Username is required for search"); + return; + } + + await window.backendAPI.fetchData("/api/search", { + username, + ssoToken, + platform, + sanitize, + replaceKeys + }); +}); + +// Function to handle time and duration conversion +function displayResults(data) { + const resultsElement = document.getElementById("results"); + const downloadContainer = document.getElementById("download-container"); + + // Apply time conversion if enabled + const convertTime = document.getElementById('convertTimeOption').checked; + const replaceKeys = document.getElementById('replaceKeysOption').checked; + let displayData = data; + + if (convertTime || replaceKeys) { + const timezone = document.getElementById('timezoneSelect').value; + displayData = window.backendAPI.processTimestamps(structuredClone(data), timezone); // Use structured clone API instead of JSON.parse/stringify + // displayData = window.backendAPI.processTimestamps(JSON.parse(JSON.stringify(data)), timezone); + } + + // Format the data + let formattedData = ''; + if (window.appState.outputFormat === 'yaml') { + formattedData = window.backendAPI.jsonToYAML(displayData); + document.getElementById("downloadJson").textContent = "Download YAML Data"; + } else { + formattedData = JSON.stringify(displayData, null, 2); + document.getElementById("downloadJson").textContent = "Download JSON Data"; + } + + resultsElement.textContent = formattedData; + resultsElement.style.display = "block"; + downloadContainer.style.display = "block"; +} + +// Helper function to display errors +function displayError(message) { + const errorElement = document.getElementById("error"); + const loadingElement = document.getElementById("loading"); + const resultsElement = document.getElementById("results"); + + errorElement.textContent = message; + loadingElement.style.display = "none"; + + // Clear previous results to ensure they can be redrawn + resultsElement.style.display = "none"; + resultsElement.textContent = ""; + + // Keep tutorial hidden if previously dismissed + if (window.appState.tutorialDismissed) { + document.querySelectorAll(".tutorial").forEach(element => { + element.style.display = "none"; + }); + } +} + +function addEnterKeyListeners() { + // Use event delegation for handling Enter key press + document.addEventListener("keypress", function(event) { + if (event.key === "Enter") { + // Get the active element + const activeElement = document.activeElement; + + if (!activeElement || !activeElement.id) return; + + // Mapping of input fields to their submit buttons + const inputToButtonMapping = { + "ssoToken": null, // Will trigger active tab button + "username": null, // Will trigger active tab button + "matchUsername": "fetchMatches", + "matchId": "fetchMatchInfo", + "userUsername": "fetchUserInfo", + "searchUsername": "fuzzySearch" + }; + + if (activeElement.id in inputToButtonMapping) { + if (inputToButtonMapping[activeElement.id]) { + // Click the specific button + document.getElementById(inputToButtonMapping[activeElement.id]).click(); + } else { + // Trigger the active tab button + triggerActiveTabButton(); + } + } + } + }); +} + +function triggerActiveTabButton() { + const activeTab = document.querySelector(".tab.active").getAttribute("data-tab"); + switch (activeTab) { + case "stats": + document.getElementById("fetchStats").click(); + break; + case "matches": + document.getElementById("fetchMatches").click(); + break; + case "user": + document.getElementById("fetchUserInfo").click(); + break; + case "other": + document.getElementById("fuzzySearch").click(); + break; + } +} + +// Time options +function setupTimeOptions() { + const convertTimeCheckbox = document.getElementById('convertTimeOption'); + const timezoneSelect = document.getElementById('timezoneSelect'); + + convertTimeCheckbox.addEventListener('change', function() { + timezoneSelect.disabled = !this.checked; + + if ((window.appState.currentData)) { + displayResults((window.appState.currentData)); // Refresh the display + } + }); + + timezoneSelect.addEventListener('change', function() { + if ((window.appState.currentData)) { + displayResults((window.appState.currentData)); // Refresh the display + } + }); +} + +// Download Button +function setupDownloadButton() { + const downloadBtn = document.getElementById("downloadJson"); + if (!downloadBtn) return; + + downloadBtn.addEventListener("click", function() { + const resultsElement = document.getElementById("results"); + const jsonData = resultsElement.textContent; + + if (!jsonData) { + alert("No data to download"); + return; + } + + // Create a Blob with the data + const contentType = window.appState.outputFormat === 'yaml' ? 'text/yaml' : 'application/json'; + const blob = new Blob([jsonData], { type: contentType }); + + // Create a temporary link element + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + + // Generate a filename with timestamp + const date = new Date(); + const timestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}`; + const extension = window.appState.outputFormat === 'yaml' ? 'yaml' : 'json'; + a.download = `cod_stats_${timestamp}.${extension}`; + + // Trigger download + document.body.appendChild(a); + a.click(); + + // Clean up + document.body.removeChild(a); + }); +} +// Function to synchronize username across tabs +function syncUsernames() { + const mainUsername = document.getElementById("username").value.trim(); + + // Only sync if there's a value + if (mainUsername) { + document.getElementById("matchUsername").value = mainUsername; + document.getElementById("userUsername").value = mainUsername; + document.getElementById("searchUsername").value = mainUsername; + } + + // Also sync platform across tabs when it changes + const mainPlatform = document.getElementById("platform").value; + document.getElementById("matchPlatform").value = mainPlatform; + document.getElementById("userPlatform").value = mainPlatform; + document.getElementById("searchPlatform").value = mainPlatform; +} + +// Sync listeners for persistent usernames +function addSyncListeners() { + // Add change listeners for username sync + document.getElementById("username").addEventListener("change", syncUsernames); + document.getElementById("matchUsername").addEventListener("change", function() { + document.getElementById("username").value = this.value; + syncUsernames(); + }); + document.getElementById("userUsername").addEventListener("change", function() { + document.getElementById("username").value = this.value; + syncUsernames(); + }); + document.getElementById("searchUsername").addEventListener("change", function() { + document.getElementById("username").value = this.value; + syncUsernames(); + }); + + // Add change listeners for platform sync + document.getElementById("platform").addEventListener("change", syncUsernames); +}