From d44ec46ae5729f8617efc55b02611ebfe11a8a06 Mon Sep 17 00:00:00 2001 From: Nicholas Mackowski Date: Wed, 5 Mar 2025 20:46:53 -0500 Subject: [PATCH 1/2] fixed errors in view your climbs --- components/ClimbingDashboard.vue | 34 +++- components/ClimbingDashboardSelfService.vue | 93 +++++++---- pages/view_your_climbs/index.vue | 4 +- server/api/tick-export-self-service.js | 171 +++++++++++++++++--- 4 files changed, 250 insertions(+), 52 deletions(-) diff --git a/components/ClimbingDashboard.vue b/components/ClimbingDashboard.vue index e343260..02cb9f0 100644 --- a/components/ClimbingDashboard.vue +++ b/components/ClimbingDashboard.vue @@ -1,7 +1,7 @@ diff --git a/server/api/tick-export-self-service.js b/server/api/tick-export-self-service.js index e44831c..9f69299 100644 --- a/server/api/tick-export-self-service.js +++ b/server/api/tick-export-self-service.js @@ -1,11 +1,15 @@ import Papa from "papaparse"; export default defineEventHandler(async (event) => { + // Get query parameters and log them for debugging const query = getQuery(event); const userId = query.userId; const userName = query.userName; + console.log(`Received request for userId: ${userId}, userName: ${userName}`); + if (!userId || !userName) { + console.log("Missing required parameters"); return { error: "Missing required parameters", details: "Both userId and userName are required", @@ -13,11 +17,51 @@ export default defineEventHandler(async (event) => { } const url = `https://www.mountainproject.com/user/${userId}/${userName}/tick-export`; + console.log(`Fetching data from: ${url}`); try { - const csvText = await $fetch(url); + // Add timeout and retry logic for more reliable fetching + const fetchWithTimeout = async (url, options = {}, timeout = 10000) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + try { + const response = await $fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + return response; + } catch (error) { + clearTimeout(id); + throw error; + } + }; + + // Try up to 3 times with increasing timeouts + let csvText; + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + try { + csvText = await fetchWithTimeout(url, {}, 10000 * (attempts + 1)); + break; + } catch (error) { + attempts++; + console.log(`Attempt ${attempts} failed: ${error.message}`); + if (attempts >= maxAttempts) throw error; + // Wait before retry + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } - if (csvText.includes("") || csvText.includes("")) { + // Check if response is HTML instead of CSV + if ( + typeof csvText === "string" && + (csvText.includes("") || csvText.includes("")) + ) { + console.log("Received HTML instead of CSV"); return { error: "Invalid user data", details: @@ -25,12 +69,43 @@ export default defineEventHandler(async (event) => { }; } - const parsed = Papa.parse(csvText, { header: true, dynamicTyping: true }); + // Check if we got data + if (!csvText || csvText.trim() === "") { + console.log("Received empty response"); + return { + error: "Empty response", + details: + "No data received from Mountain Project. Please check your User ID and Username.", + }; + } - const rows = parsed.data.filter( - (row) => row.Date && row.Route && row.Rating + // Log first few characters for debugging + console.log( + `Received CSV data (first 100 chars): ${csvText.substring(0, 100)}...` ); + // Parse CSV with more robust options + const parsed = Papa.parse(csvText, { + header: true, + dynamicTyping: true, + skipEmptyLines: true, + transformHeader: (header) => header.trim(), + error: (error) => { + console.log(`CSV parsing error: ${error}`); + }, + }); + + if (parsed.errors && parsed.errors.length > 0) { + console.log(`CSV parsing errors: ${JSON.stringify(parsed.errors)}`); + } + + // Filter valid rows - be more lenient with empty fields + const rows = parsed.data.filter((row) => { + return row && typeof row === "object" && Object.keys(row).length > 0; + }); + + console.log(`Parsed ${rows.length} valid rows`); + if (!rows.length) { return { error: "No climbing data", @@ -39,27 +114,64 @@ export default defineEventHandler(async (event) => { }; } + // Process the data const total_climbs = rows.length; - const sendStyles = ["Onsight", "Flash", "Redpoint"]; - const total_sends = rows.filter((row) => - sendStyles.includes(row["Lead Style"]) - ).length; + // Normalize grade function to handle grades with slashes (e.g., "5.11a/b" -> "5.11b") + const normalizeGrade = (grade) => { + if (!grade || typeof grade !== "string") return grade; - const send_rows = rows.filter((row) => - sendStyles.includes(row["Lead Style"]) - ); + // If the grade contains a slash, take the higher grade + if (grade.includes("/")) { + const parts = grade.split("/"); + return parts[0].substring(0, parts[0].length - 1) + parts[1]; + } + + return grade; + }; + + // Be more flexible with field names by normalizing/checking multiple possible names + const getLedStyle = (row) => { + const possibleFields = [ + "Lead Style", + "LeadStyle", + "leadstyle", + "Lead_Style", + ]; + for (const field of possibleFields) { + if (row[field] !== undefined) return row[field]; + } + return null; + }; + + const sendStyles = ["Onsight", "Flash", "Redpoint", "Pinkpoint"]; + const total_sends = rows.filter((row) => { + const style = getLedStyle(row); + return style && sendStyles.includes(style); + }).length; + + const send_rows = rows.filter((row) => { + const style = getLedStyle(row); + return style && sendStyles.includes(style); + }); const gradeCounts = {}; rows.forEach((row) => { - if (!gradeCounts[row.Rating]) gradeCounts[row.Rating] = 0; - gradeCounts[row.Rating]++; + if (row.Rating) { + const normalizedGrade = normalizeGrade(row.Rating); + if (!gradeCounts[normalizedGrade]) gradeCounts[normalizedGrade] = 0; + gradeCounts[normalizedGrade]++; + } }); const sendGradeCounts = {}; send_rows.forEach((row) => { - if (!sendGradeCounts[row.Rating]) sendGradeCounts[row.Rating] = 0; - sendGradeCounts[row.Rating]++; + if (row.Rating) { + const normalizedGrade = normalizeGrade(row.Rating); + if (!sendGradeCounts[normalizedGrade]) + sendGradeCounts[normalizedGrade] = 0; + sendGradeCounts[normalizedGrade]++; + } }); const gradesArray = Object.entries(gradeCounts).sort((a, b) => b[1] - a[1]); @@ -67,15 +179,34 @@ export default defineEventHandler(async (event) => { (a, b) => b[1] - a[1] ); - rows.sort((a, b) => new Date(b.Date) - new Date(a.Date)); + // Sort by date for recent climbs, handle various date formats + const parseDate = (dateStr) => { + if (!dateStr) return new Date(0); + try { + return new Date(dateStr); + } catch (e) { + return new Date(0); + } + }; + + rows.sort((a, b) => { + const dateA = parseDate(a.Date); + const dateB = parseDate(b.Date); + return dateB - dateA; + }); + const recent_climbs = rows.slice(0, 5); const climbs_over_time = {}; rows.forEach((row) => { - if (!climbs_over_time[row.Date]) climbs_over_time[row.Date] = 0; - climbs_over_time[row.Date]++; + if (row.Date) { + if (!climbs_over_time[row.Date]) climbs_over_time[row.Date] = 0; + climbs_over_time[row.Date]++; + } }); + // Return the processed data + console.log("Successfully processed data, returning results"); return { total_climbs, total_sends, @@ -88,7 +219,7 @@ export default defineEventHandler(async (event) => { console.error("Error fetching Mountain Project data:", error); return { error: "Failed to fetch data", - details: "Could not connect to Mountain Project. Please try again later.", + details: `Error: ${error.message}. Please check your User ID and Username, and try again later.`, }; } }); From 60f65e66c3cadcd3cf6fe277402843117a980ff6 Mon Sep 17 00:00:00 2001 From: Nicholas Mackowski Date: Sat, 8 Mar 2025 18:19:30 -0500 Subject: [PATCH 2/2] changed recent to all climbs' --- components/ClimbingDashboard.vue | 14 +++++++------- server/api/tick-export-self-service.js | 13 ------------- server/api/tick-export.js | 6 +++--- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/components/ClimbingDashboard.vue b/components/ClimbingDashboard.vue index 02cb9f0..779a818 100644 --- a/components/ClimbingDashboard.vue +++ b/components/ClimbingDashboard.vue @@ -42,11 +42,11 @@ -
-

Recent Sport Climbs

+
+

Sport Climbs

  • @@ -70,7 +70,7 @@ export default { totalClimbs: "--", totalSends: "--", commonGrade: "--", - recentClimbs: [], + orderedClimbs: [], gradeData: [], sendGradeData: [], showSendsOnly: true, @@ -104,7 +104,7 @@ export default { ? data.grades[0][0] : "--"; - this.recentClimbs = data.recent_climbs; + this.orderedClimbs = data.ordered_climbs; // Store both all attempts and sends-only grade data this.gradeData = data.grades || []; @@ -366,14 +366,14 @@ export default { font-size: 14px; color: #555; } -.recent-climbs { +.ordered-climbs { background-color: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } -.recent-climbs h3 { +.ordered-climbs h3 { margin-top: 0; color: #555; } diff --git a/server/api/tick-export-self-service.js b/server/api/tick-export-self-service.js index 9f69299..75b7766 100644 --- a/server/api/tick-export-self-service.js +++ b/server/api/tick-export-self-service.js @@ -56,19 +56,6 @@ export default defineEventHandler(async (event) => { } } - // Check if response is HTML instead of CSV - if ( - typeof csvText === "string" && - (csvText.includes("") || csvText.includes("")) - ) { - console.log("Received HTML instead of CSV"); - return { - error: "Invalid user data", - details: - "Could not retrieve climbing data. Please check your User ID and Username.", - }; - } - // Check if we got data if (!csvText || csvText.trim() === "") { console.log("Received empty response"); diff --git a/server/api/tick-export.js b/server/api/tick-export.js index dd52e5f..f47af59 100644 --- a/server/api/tick-export.js +++ b/server/api/tick-export.js @@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => { const total_climbs = rows.length; - const sendStyles = ["Onsight", "Flash", "Redpoint"]; + const sendStyles = ["Onsight", "Flash", "Redpoint", "Pinkpoint"]; const total_sends = rows.filter((row) => sendStyles.includes(row["Lead Style"]) ).length; @@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => { ); rows.sort((a, b) => new Date(b.Date) - new Date(a.Date)); - const recent_climbs = rows.slice(0, 5); + const ordered_climbs = rows; const climbs_over_time = {}; rows.forEach((row) => { @@ -55,7 +55,7 @@ export default defineEventHandler(async (event) => { total_sends, grades: gradesArray, send_grades: sendGradesArray, - recent_climbs, + ordered_climbs, climbs_over_time, }; } catch (error) {